1pub mod manifest;
10pub mod manifest_feature_flags;
11
12use arrow::array::Float32Array;
13use arrow::record_batch::RecordBatchIterator;
14use arrow_ipc::reader::StreamReader;
15use async_trait::async_trait;
16use bytes::Bytes;
17use futures::{StreamExt, TryStreamExt};
18use lance::dataset::builder::DatasetBuilder;
19use lance::dataset::refs::check_valid_branch;
20use lance::dataset::scanner::Scanner;
21use lance::dataset::statistics::DatasetStatisticsExt;
22use lance::dataset::transaction::{Operation, Transaction};
23use lance::dataset::{
24 Dataset, MergeInsertBuilder, WhenMatched, WhenNotMatched, WhenNotMatchedBySource, WriteMode,
25 WriteParams,
26};
27use lance::index::{DatasetIndexExt, IndexParams, vector::VectorIndexParams};
28use lance::session::Session;
29use lance_index::scalar::{
30 BuiltinIndexType, FullTextSearchQuery, InvertedIndexParams, ScalarIndexParams,
31};
32use lance_index::vector::{
33 bq::RQBuildParams, hnsw::builder::HnswBuildParams, ivf::IvfBuildParams, pq::PQBuildParams,
34 sq::builder::SQBuildParams,
35};
36use lance_index::{IndexType, is_system_index};
37use lance_io::object_store::{ObjectStore, ObjectStoreParams, ObjectStoreRegistry};
38use lance_linalg::distance::MetricType;
39use lance_table::io::commit::{ManifestNamingScheme, VERSIONS_DIR};
40use object_store::ObjectStoreExt;
41use object_store::path::Path;
42use object_store::{Error as ObjectStoreError, ObjectStore as OSObjectStore, PutMode, PutOptions};
43use std::collections::HashMap;
44use std::io::Cursor;
45use std::sync::{Arc, Mutex};
46
47use crate::context::DynamicContextProvider;
48use lance_namespace::models::{
49 AnalyzeTableQueryPlanRequest, BatchDeleteTableVersionsRequest,
50 BatchDeleteTableVersionsResponse, BranchContents as ModelBranchContents, CountTableRowsRequest,
51 CreateNamespaceRequest, CreateNamespaceResponse, CreateTableBranchRequest,
52 CreateTableBranchResponse, CreateTableIndexRequest, CreateTableIndexResponse,
53 CreateTableRequest, CreateTableResponse, CreateTableScalarIndexResponse, CreateTableTagRequest,
54 CreateTableTagResponse, CreateTableVersionRequest, CreateTableVersionResponse,
55 DeclareTableRequest, DeclareTableResponse, DeleteTableBranchRequest, DeleteTableBranchResponse,
56 DeleteTableTagRequest, DeleteTableTagResponse, DescribeNamespaceRequest,
57 DescribeNamespaceResponse, DescribeTableIndexStatsRequest, DescribeTableIndexStatsResponse,
58 DescribeTableRequest, DescribeTableResponse, DescribeTableVersionRequest,
59 DescribeTableVersionResponse, DescribeTransactionRequest, DescribeTransactionResponse,
60 DropNamespaceRequest, DropNamespaceResponse, DropTableIndexRequest, DropTableIndexResponse,
61 DropTableRequest, DropTableResponse, ExplainTableQueryPlanRequest, FragmentStats,
62 FragmentSummary, GetTableStatsRequest, GetTableStatsResponse, GetTableTagVersionRequest,
63 GetTableTagVersionResponse, Identity, IndexContent, InsertIntoTableRequest,
64 InsertIntoTableResponse, ListNamespacesRequest, ListNamespacesResponse,
65 ListTableBranchesRequest, ListTableBranchesResponse, ListTableIndicesRequest,
66 ListTableIndicesResponse, ListTableTagsRequest, ListTableTagsResponse,
67 ListTableVersionsRequest, ListTableVersionsResponse, ListTablesRequest, ListTablesResponse,
68 MergeInsertIntoTableRequest, MergeInsertIntoTableResponse, NamespaceExistsRequest,
69 QueryTableRequest, QueryTableRequestColumns, QueryTableRequestVector, RestoreTableRequest,
70 RestoreTableResponse, TableExistsRequest, TableVersion, TagContents as ModelTagContents,
71 UpdateTableSchemaMetadataRequest, UpdateTableSchemaMetadataResponse, UpdateTableTagRequest,
72 UpdateTableTagResponse,
73};
74
75use lance_core::{Error, Result};
76use lance_namespace::LanceNamespace;
77use lance_namespace::error::NamespaceError;
78use lance_namespace::schema::arrow_schema_to_json;
79
80use crate::credentials::{
81 CredentialVendor, create_credential_vendor_for_location, has_credential_vendor_config,
82};
83
84#[derive(Debug, Default)]
89pub struct OpsMetrics {
90 counters: Mutex<HashMap<String, u64>>,
91}
92
93impl OpsMetrics {
94 pub fn increment(&self, operation: &str) {
96 if let Ok(mut counters) = self.counters.lock() {
97 *counters.entry(operation.to_string()).or_insert(0) += 1;
98 }
99 }
100
101 pub fn retrieve(&self) -> HashMap<String, u64> {
103 self.counters.lock().map(|c| c.clone()).unwrap_or_default()
104 }
105
106 pub fn reset(&self) {
108 if let Ok(mut counters) = self.counters.lock() {
109 counters.clear();
110 }
111 }
112}
113
114pub(crate) struct TableStatus {
119 pub(crate) exists: bool,
121 pub(crate) is_deregistered: bool,
123 pub(crate) has_reserved_file: bool,
125}
126
127enum DirectoryIndexParams {
128 Scalar {
129 index_type: IndexType,
130 params: ScalarIndexParams,
131 },
132 Inverted(InvertedIndexParams),
133 Vector {
134 index_type: IndexType,
135 params: VectorIndexParams,
136 },
137}
138
139impl DirectoryIndexParams {
140 fn index_type(&self) -> IndexType {
141 match self {
142 Self::Scalar { index_type, .. } | Self::Vector { index_type, .. } => *index_type,
143 Self::Inverted(_) => IndexType::Inverted,
144 }
145 }
146
147 fn params(&self) -> &dyn IndexParams {
148 match self {
149 Self::Scalar { params, .. } => params,
150 Self::Inverted(params) => params,
151 Self::Vector { params, .. } => params,
152 }
153 }
154}
155
156#[derive(Clone)]
191pub struct DirectoryNamespaceBuilder {
192 root: String,
193 storage_options: Option<HashMap<String, String>>,
194 session: Option<Arc<Session>>,
195 manifest_enabled: bool,
196 dir_listing_enabled: bool,
197 inline_optimization_enabled: bool,
198 table_version_tracking_enabled: bool,
199 dir_listing_to_manifest_migration_enabled: bool,
204 credential_vendor_properties: HashMap<String, String>,
205 context_provider: Option<Arc<dyn DynamicContextProvider>>,
206 commit_retries: Option<u32>,
207 vend_input_storage_options: bool,
210 vend_input_storage_options_refresh_interval_millis: Option<u64>,
215 ops_metrics_enabled: bool,
217}
218
219impl std::fmt::Debug for DirectoryNamespaceBuilder {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 f.debug_struct("DirectoryNamespaceBuilder")
222 .field("root", &self.root)
223 .field("storage_options", &self.storage_options)
224 .field("manifest_enabled", &self.manifest_enabled)
225 .field("dir_listing_enabled", &self.dir_listing_enabled)
226 .field(
227 "inline_optimization_enabled",
228 &self.inline_optimization_enabled,
229 )
230 .field(
231 "table_version_tracking_enabled",
232 &self.table_version_tracking_enabled,
233 )
234 .field(
235 "dir_listing_to_manifest_migration_enabled",
236 &self.dir_listing_to_manifest_migration_enabled,
237 )
238 .field(
239 "context_provider",
240 &self.context_provider.as_ref().map(|_| "Some(...)"),
241 )
242 .field(
243 "vend_input_storage_options",
244 &self.vend_input_storage_options,
245 )
246 .field(
247 "vend_input_storage_options_refresh_interval_millis",
248 &self.vend_input_storage_options_refresh_interval_millis,
249 )
250 .field("ops_metrics_enabled", &self.ops_metrics_enabled)
251 .finish()
252 }
253}
254
255impl DirectoryNamespaceBuilder {
256 pub fn new(root: impl Into<String>) -> Self {
262 Self {
263 root: root.into().trim_end_matches('/').to_string(),
264 storage_options: None,
265 session: None,
266 manifest_enabled: true,
267 dir_listing_enabled: true, inline_optimization_enabled: true,
269 table_version_tracking_enabled: false, dir_listing_to_manifest_migration_enabled: false, credential_vendor_properties: HashMap::new(),
272 context_provider: None,
273 commit_retries: None,
274 vend_input_storage_options: false,
275 vend_input_storage_options_refresh_interval_millis: None,
276 ops_metrics_enabled: false,
277 }
278 }
279
280 pub fn manifest_enabled(mut self, enabled: bool) -> Self {
285 self.manifest_enabled = enabled;
286 self
287 }
288
289 pub fn dir_listing_enabled(mut self, enabled: bool) -> Self {
294 self.dir_listing_enabled = enabled;
295 self
296 }
297
298 pub fn dir_listing_to_manifest_migration_enabled(mut self, enabled: bool) -> Self {
305 self.dir_listing_to_manifest_migration_enabled = enabled;
306 self
307 }
308
309 pub fn inline_optimization_enabled(mut self, enabled: bool) -> Self {
314 self.inline_optimization_enabled = enabled;
315 self
316 }
317
318 pub fn table_version_tracking_enabled(mut self, enabled: bool) -> Self {
326 self.table_version_tracking_enabled = enabled;
327 self
328 }
329
330 pub fn from_properties(
398 properties: HashMap<String, String>,
399 session: Option<Arc<Session>>,
400 ) -> Result<Self> {
401 let root = properties.get("root").cloned().ok_or_else(|| {
403 lance_core::Error::from(NamespaceError::InvalidInput {
404 message: "Missing required property 'root' for directory namespace".to_string(),
405 })
406 })?;
407
408 let storage_options: HashMap<String, String> = properties
410 .iter()
411 .filter_map(|(k, v)| {
412 k.strip_prefix("storage.")
413 .map(|key| (key.to_string(), v.clone()))
414 })
415 .collect();
416
417 let storage_options = if storage_options.is_empty() {
418 None
419 } else {
420 Some(storage_options)
421 };
422
423 let manifest_enabled = properties
425 .get("manifest_enabled")
426 .and_then(|v| v.parse::<bool>().ok())
427 .unwrap_or(true);
428
429 let dir_listing_enabled = properties
431 .get("dir_listing_enabled")
432 .and_then(|v| v.parse::<bool>().ok())
433 .unwrap_or(true);
434
435 let inline_optimization_enabled = properties
437 .get("inline_optimization_enabled")
438 .and_then(|v| v.parse::<bool>().ok())
439 .unwrap_or(true);
440
441 let table_version_tracking_enabled = properties
443 .get("table_version_tracking_enabled")
444 .and_then(|v| v.parse::<bool>().ok())
445 .unwrap_or(false);
446
447 let dir_listing_to_manifest_migration_enabled = properties
449 .get("dir_listing_to_manifest_migration_enabled")
450 .and_then(|v| v.parse::<bool>().ok())
451 .unwrap_or(false);
452
453 let credential_vendor_properties: HashMap<String, String> = properties
457 .iter()
458 .filter_map(|(k, v)| {
459 k.strip_prefix("credential_vendor.")
460 .map(|key| (key.to_string(), v.clone()))
461 })
462 .collect();
463
464 let commit_retries = properties
465 .get("commit_retries")
466 .and_then(|v| v.parse::<u32>().ok());
467
468 let vend_input_storage_options = properties
470 .get("vend_input_storage_options")
471 .and_then(|v| v.parse::<bool>().ok())
472 .unwrap_or(false);
473
474 let vend_input_storage_options_refresh_interval_millis = properties
476 .get("vend_input_storage_options_refresh_interval_millis")
477 .and_then(|v| v.parse::<u64>().ok());
478
479 let ops_metrics_enabled = properties
481 .get("ops_metrics_enabled")
482 .and_then(|v| v.parse::<bool>().ok())
483 .unwrap_or(false);
484
485 Ok(Self {
486 root: root.trim_end_matches('/').to_string(),
487 storage_options,
488 session,
489 manifest_enabled,
490 dir_listing_enabled,
491 inline_optimization_enabled,
492 table_version_tracking_enabled,
493 dir_listing_to_manifest_migration_enabled,
494 credential_vendor_properties,
495 context_provider: None,
496 commit_retries,
497 vend_input_storage_options,
498 vend_input_storage_options_refresh_interval_millis,
499 ops_metrics_enabled,
500 })
501 }
502
503 pub fn storage_option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
510 self.storage_options
511 .get_or_insert_with(HashMap::new)
512 .insert(key.into(), value.into());
513 self
514 }
515
516 pub fn storage_options(mut self, options: HashMap<String, String>) -> Self {
522 self.storage_options
523 .get_or_insert_with(HashMap::new)
524 .extend(options);
525 self
526 }
527
528 pub fn session(mut self, session: Arc<Session>) -> Self {
538 self.session = Some(session);
539 self
540 }
541
542 pub fn commit_retries(mut self, retries: u32) -> Self {
545 self.commit_retries = Some(retries);
546 self
547 }
548
549 pub fn credential_vendor_property(
577 mut self,
578 key: impl Into<String>,
579 value: impl Into<String>,
580 ) -> Self {
581 self.credential_vendor_properties
582 .insert(key.into(), value.into());
583 self
584 }
585
586 pub fn credential_vendor_properties(mut self, properties: HashMap<String, String>) -> Self {
594 self.credential_vendor_properties.extend(properties);
595 self
596 }
597
598 pub fn context_provider(mut self, provider: Arc<dyn DynamicContextProvider>) -> Self {
608 self.context_provider = Some(provider);
609 self
610 }
611
612 pub fn vend_input_storage_options(mut self, enabled: bool) -> Self {
621 self.vend_input_storage_options = enabled;
622 self
623 }
624
625 pub fn vend_input_storage_options_refresh_interval_millis(
637 mut self,
638 interval_millis: u64,
639 ) -> Self {
640 self.vend_input_storage_options_refresh_interval_millis = Some(interval_millis);
641 self
642 }
643
644 pub fn ops_metrics_enabled(mut self, enabled: bool) -> Self {
652 self.ops_metrics_enabled = enabled;
653 self
654 }
655
656 pub async fn build(self) -> Result<DirectoryNamespace> {
669 let (object_store, base_path) =
670 Self::initialize_object_store(&self.root, &self.storage_options, &self.session).await?;
671
672 let manifest_ns = if self.manifest_enabled {
673 match manifest::ManifestNamespace::from_directory(
674 self.root.clone(),
675 self.storage_options.clone(),
676 self.session.clone(),
677 object_store.clone(),
678 base_path.clone(),
679 self.dir_listing_enabled,
680 self.inline_optimization_enabled,
681 self.commit_retries,
682 )
683 .await
684 {
685 Ok(ns) => Some(Arc::new(ns)),
686 Err(e) if manifest_feature_flags::is_incompatible_manifest_error(&e) => {
687 return Err(e);
691 }
692 Err(e) => {
693 log::warn!(
695 "Failed to initialize manifest namespace, falling back to directory listing only: {}",
696 e
697 );
698 None
699 }
700 }
701 } else {
702 None
703 };
704
705 let credential_vendor = if has_credential_vendor_config(&self.credential_vendor_properties)
707 {
708 create_credential_vendor_for_location(&self.root, &self.credential_vendor_properties)
709 .await?
710 .map(Arc::from)
711 } else {
712 None
713 };
714
715 let ops_metrics = if self.ops_metrics_enabled {
716 Some(Arc::new(OpsMetrics::default()))
717 } else {
718 None
719 };
720
721 Ok(DirectoryNamespace {
722 root: self.root,
723 storage_options: self.storage_options,
724 session: self.session,
725 object_store,
726 base_path,
727 manifest_ns,
728 dir_listing_enabled: self.dir_listing_enabled,
729 dir_listing_to_manifest_migration_enabled: self
730 .dir_listing_to_manifest_migration_enabled,
731 table_version_tracking_enabled: self.table_version_tracking_enabled,
732 credential_vendor,
733 context_provider: self.context_provider,
734 vend_input_storage_options: self.vend_input_storage_options,
735 vend_input_storage_options_refresh_interval_millis: self
736 .vend_input_storage_options_refresh_interval_millis,
737 ops_metrics,
738 })
739 }
740
741 async fn initialize_object_store(
743 root: &str,
744 storage_options: &Option<HashMap<String, String>>,
745 session: &Option<Arc<Session>>,
746 ) -> Result<(Arc<ObjectStore>, Path)> {
747 let accessor = storage_options.clone().map(|opts| {
749 Arc::new(lance_io::object_store::StorageOptionsAccessor::with_static_options(opts))
750 });
751 let params = ObjectStoreParams {
752 storage_options_accessor: accessor,
753 ..Default::default()
754 };
755
756 let registry = if let Some(session) = session {
758 session.store_registry()
759 } else {
760 Arc::new(ObjectStoreRegistry::default())
761 };
762
763 let (object_store, base_path) = ObjectStore::from_uri_and_params(registry, root, ¶ms)
765 .await
766 .map_err(|e| {
767 lance_core::Error::from(NamespaceError::Internal {
768 message: format!("Failed to create object store: {:?}", e),
769 })
770 })?;
771
772 Ok((object_store, base_path))
773 }
774}
775
776pub struct DirectoryNamespace {
800 root: String,
801 storage_options: Option<HashMap<String, String>>,
802 session: Option<Arc<Session>>,
803 object_store: Arc<ObjectStore>,
804 base_path: Path,
805 manifest_ns: Option<Arc<manifest::ManifestNamespace>>,
806 dir_listing_enabled: bool,
807 dir_listing_to_manifest_migration_enabled: bool,
811 table_version_tracking_enabled: bool,
814 credential_vendor: Option<Arc<dyn CredentialVendor>>,
817 #[allow(dead_code)]
820 context_provider: Option<Arc<dyn DynamicContextProvider>>,
821 vend_input_storage_options: bool,
823 vend_input_storage_options_refresh_interval_millis: Option<u64>,
826 ops_metrics: Option<Arc<OpsMetrics>>,
828}
829
830impl std::fmt::Debug for DirectoryNamespace {
831 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
832 write!(f, "{}", self.namespace_id())
833 }
834}
835
836impl std::fmt::Display for DirectoryNamespace {
837 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
838 write!(f, "{}", self.namespace_id())
839 }
840}
841
842struct TableDeleteEntry {
845 table_id: Option<Vec<String>>,
846 ranges: Vec<(i64, i64)>,
847}
848
849impl DirectoryNamespace {
850 fn apply_pagination(
863 names: &mut Vec<String>,
864 page_token: Option<String>,
865 limit: Option<i32>,
866 ) -> Option<String> {
867 names.sort();
869
870 if let Some(start_after) = page_token {
872 if let Some(index) = names
873 .iter()
874 .position(|name| name.as_str() > start_after.as_str())
875 {
876 names.drain(0..index);
877 } else {
878 names.clear();
879 }
880 }
881
882 if let Some(limit) = limit
884 && limit >= 0
885 {
886 let limit = limit as usize;
887 if names.len() > limit {
888 let next_page_token = if limit > 0 {
889 Some(names[limit - 1].clone())
890 } else {
891 None
892 };
893 names.truncate(limit);
894 return next_page_token;
895 }
896 }
897
898 None
899 }
900
901 async fn list_directory_tables(&self) -> Result<Vec<String>> {
903 let mut tables = Vec::new();
904 let entries = self
905 .object_store
906 .read_dir(self.base_path.clone())
907 .await
908 .map_err(|e| {
909 lance_core::Error::from(NamespaceError::Internal {
910 message: format!("Failed to list directory: {:?}", e),
911 })
912 })?;
913
914 for entry in entries {
915 let path = entry.trim_end_matches('/');
916 if !path.ends_with(".lance") {
917 continue;
918 }
919
920 let table_name = &path[..path.len() - 6];
921
922 let status = self.check_table_status(table_name).await;
924 if status.is_deregistered {
925 continue;
926 }
927
928 tables.push(table_name.to_string());
929 }
930
931 Ok(tables)
932 }
933
934 fn validate_root_namespace_id(id: &Option<Vec<String>>) -> Result<()> {
936 if let Some(id) = id
937 && !id.is_empty()
938 {
939 return Err(NamespaceError::Unsupported {
940 message: format!(
941 "Directory namespace only supports root namespace operations, but got namespace ID: {:?}. Expected empty ID.",
942 id
943 ),
944 }
945 .into());
946 }
947 Ok(())
948 }
949
950 fn table_name_from_id(id: &Option<Vec<String>>) -> Result<String> {
952 let id = id.as_ref().ok_or_else(|| {
953 lance_core::Error::from(NamespaceError::InvalidInput {
954 message: "Directory namespace table ID cannot be empty".to_string(),
955 })
956 })?;
957
958 if id.len() != 1 {
959 return Err(NamespaceError::Unsupported {
960 message: format!(
961 "Multi-level table IDs are only supported when manifest mode is enabled, but got: {:?}",
962 id
963 ),
964 }
965 .into());
966 }
967
968 Ok(id[0].clone())
969 }
970
971 fn format_table_id(table_id: &[String]) -> String {
972 format!(
973 "table id '{}'",
974 manifest::ManifestNamespace::str_object_id(table_id)
975 )
976 }
977
978 fn format_table_id_from_request(id: &Option<Vec<String>>) -> String {
979 id.as_ref()
980 .map(|table_id| Self::format_table_id(table_id))
981 .unwrap_or_else(|| "table id '<unknown>'".to_string())
982 }
983
984 async fn resolve_table_location(&self, id: &Option<Vec<String>>) -> Result<String> {
985 let mut describe_req = DescribeTableRequest::new();
986 describe_req.id = id.clone();
987 describe_req.load_detailed_metadata = Some(false);
988
989 let describe_resp = self.describe_table_impl(describe_req).await?;
991
992 describe_resp.location.ok_or_else(|| {
993 lance_core::Error::from(NamespaceError::TableNotFound {
994 message: format!("Table location not found for: {:?}", id),
995 })
996 })
997 }
998
999 fn map_tag_error(err: lance_core::Error, tag: &str, table_uri: &str) -> lance_core::Error {
1002 match err {
1003 lance_core::Error::RefNotFound { .. } => NamespaceError::TableTagNotFound {
1004 message: format!("tag '{}' for table at '{}'", tag, table_uri),
1005 }
1006 .into(),
1007 lance_core::Error::RefConflict { .. } => NamespaceError::TableTagAlreadyExists {
1008 message: format!("tag '{}' for table at '{}'", tag, table_uri),
1009 }
1010 .into(),
1011 lance_core::Error::InvalidRef { message } => NamespaceError::InvalidInput {
1012 message: format!("invalid tag '{}': {}", tag, message),
1013 }
1014 .into(),
1015 lance_core::Error::VersionNotFound { message } => {
1016 NamespaceError::TableVersionNotFound {
1017 message: format!(
1018 "version referenced by tag '{}' not found for table at '{}': {}",
1019 tag, table_uri, message
1020 ),
1021 }
1022 .into()
1023 }
1024 other => NamespaceError::Internal {
1025 message: format!(
1026 "tag operation failed for tag '{}' on table at '{}': {}",
1027 tag, table_uri, other
1028 ),
1029 }
1030 .into(),
1031 }
1032 }
1033
1034 fn map_branch_error(
1040 err: lance_core::Error,
1041 branch: &str,
1042 table_uri: &str,
1043 ) -> lance_core::Error {
1044 match err {
1045 lance_core::Error::RefNotFound { .. } => NamespaceError::TableBranchNotFound {
1046 message: format!("branch '{}' for table at '{}'", branch, table_uri),
1047 }
1048 .into(),
1049 lance_core::Error::InvalidRef { message } => NamespaceError::InvalidInput {
1050 message: format!("invalid branch '{}': {}", branch, message),
1051 }
1052 .into(),
1053 lance_core::Error::VersionNotFound { message } => {
1054 NamespaceError::TableVersionNotFound {
1055 message: format!(
1056 "source version for branch '{}' not found for table at '{}': {}",
1057 branch, table_uri, message
1058 ),
1059 }
1060 .into()
1061 }
1062 other => NamespaceError::Internal {
1063 message: format!(
1064 "branch operation failed for branch '{}' on table at '{}': {}",
1065 branch, table_uri, other
1066 ),
1067 }
1068 .into(),
1069 }
1070 }
1071
1072 async fn table_has_actual_manifests(&self, table_name: &str) -> Result<bool> {
1073 manifest::ManifestNamespace::path_has_actual_manifests(
1074 &self.object_store,
1075 &self.table_path(table_name),
1076 )
1077 .await
1078 }
1079
1080 async fn filter_declared_tables(
1081 &self,
1082 tables: Vec<String>,
1083 include_declared: bool,
1084 ) -> Result<Vec<String>> {
1085 if include_declared {
1086 return Ok(tables);
1087 }
1088
1089 let mut stream = futures::stream::iter(tables.into_iter().map(|table_name| async move {
1090 if self.table_has_actual_manifests(&table_name).await? {
1094 Ok::<Option<String>, Error>(Some(table_name))
1095 } else {
1096 Ok::<Option<String>, Error>(None)
1097 }
1098 }))
1099 .buffered(manifest::DECLARED_FILTER_CONCURRENCY);
1100
1101 let mut filtered = Vec::new();
1102 while let Some(result) = stream.next().await {
1103 if let Some(table_name) = result? {
1104 filtered.push(table_name);
1105 }
1106 }
1107 Ok(filtered)
1108 }
1109
1110 fn ipc_reader_from_request_data(
1111 request_data: &Bytes,
1112 operation: &str,
1113 ) -> Result<(
1114 Box<dyn arrow::record_batch::RecordBatchReader + Send>,
1115 usize,
1116 )> {
1117 if request_data.is_empty() {
1118 return Err(NamespaceError::InvalidInput {
1119 message: format!(
1120 "Request data (Arrow IPC stream) is required for {}",
1121 operation
1122 ),
1123 }
1124 .into());
1125 }
1126
1127 let cursor = Cursor::new(request_data.as_ref());
1128 let stream_reader =
1129 StreamReader::try_new(cursor, None).map_err(|e| NamespaceError::InvalidInput {
1130 message: format!("Invalid Arrow IPC stream: {}", e),
1131 })?;
1132 let arrow_schema = stream_reader.schema();
1133
1134 let mut num_rows = 0usize;
1135 let mut batches = Vec::new();
1136 for batch_result in stream_reader {
1137 let batch = batch_result.map_err(|e| NamespaceError::Internal {
1138 message: format!("Failed to read batch from IPC stream: {}", e),
1139 })?;
1140 num_rows += batch.num_rows();
1141 batches.push(batch);
1142 }
1143
1144 let reader: Box<dyn arrow::record_batch::RecordBatchReader + Send> = if batches.is_empty() {
1145 let batch = arrow::record_batch::RecordBatch::new_empty(arrow_schema.clone());
1146 Box::new(RecordBatchIterator::new(vec![Ok(batch)], arrow_schema))
1147 } else {
1148 let batch_results: Vec<_> = batches.into_iter().map(Ok).collect();
1149 Box::new(RecordBatchIterator::new(batch_results, arrow_schema))
1150 };
1151
1152 Ok((reader, num_rows))
1153 }
1154
1155 async fn table_uri_has_actual_manifests(&self, table_uri: &str) -> Result<bool> {
1156 let table_path = self.object_store_path_from_uri(table_uri)?;
1157 manifest::ManifestNamespace::path_has_actual_manifests(&self.object_store, &table_path)
1158 .await
1159 }
1160
1161 fn object_store_path_from_uri(&self, uri: &str) -> Result<Path> {
1162 let registry = self
1163 .session
1164 .as_ref()
1165 .map(|session| session.store_registry())
1166 .unwrap_or_else(|| Arc::new(ObjectStoreRegistry::default()));
1167 ObjectStore::extract_path_from_uri(registry, uri)
1168 }
1169
1170 fn normalized_branch(branch: Option<&str>) -> Result<Option<&str>> {
1175 match branch.filter(|b| !b.is_empty() && *b != "main") {
1176 Some(branch) => {
1177 check_valid_branch(branch).map_err(|e| {
1178 lance_core::Error::from(NamespaceError::InvalidInput {
1179 message: format!("invalid branch name '{}': {}", branch, e),
1180 })
1181 })?;
1182 Ok(Some(branch))
1183 }
1184 None => Ok(None),
1185 }
1186 }
1187
1188 async fn open_validated_branch(&self, table_uri: &str, branch: &str) -> Result<Dataset> {
1189 let dataset = self
1190 .configured_builder(table_uri)
1191 .with_branch(branch, None)
1192 .load()
1193 .await
1194 .map_err(|e| {
1195 lance_core::Error::from(NamespaceError::TableNotFound {
1196 message: format!(
1197 "branch '{}' not found for table at '{}': {}",
1198 branch, table_uri, e
1199 ),
1200 })
1201 })?;
1202 dataset.branches().get(branch).await.map_err(|_| {
1203 lance_core::Error::from(NamespaceError::TableNotFound {
1204 message: format!("branch '{}' not found for table at '{}'", branch, table_uri),
1205 })
1206 })?;
1207 Ok(dataset)
1208 }
1209
1210 async fn resolve_branch_location(&self, table_uri: &str, branch: &str) -> Result<String> {
1211 Ok(self
1212 .open_validated_branch(table_uri, branch)
1213 .await?
1214 .branch_location()
1215 .uri)
1216 }
1217
1218 fn validate_dir_only_properties(
1219 properties: Option<&HashMap<String, String>>,
1220 operation: &str,
1221 ) -> Result<()> {
1222 if properties.is_some_and(|properties| !properties.is_empty()) {
1226 return Err(NamespaceError::Unsupported {
1227 message: format!(
1228 "{} with non-empty table properties requires manifest_enabled=true",
1229 operation
1230 ),
1231 }
1232 .into());
1233 }
1234 Ok(())
1235 }
1236
1237 async fn write_reader_to_table(
1238 &self,
1239 table_uri: &str,
1240 reader: Box<dyn arrow::record_batch::RecordBatchReader + Send>,
1241 mode: WriteMode,
1242 extra_storage_options: Option<HashMap<String, String>>,
1243 ) -> Result<Dataset> {
1244 let mut merged_storage_options = self.storage_options.clone().unwrap_or_default();
1247 if let Some(extra_storage_options) = extra_storage_options {
1248 merged_storage_options.extend(extra_storage_options);
1249 }
1250 let store_params = (!merged_storage_options.is_empty()).then(|| ObjectStoreParams {
1251 storage_options_accessor: Some(Arc::new(
1252 lance_io::object_store::StorageOptionsAccessor::with_static_options(
1253 merged_storage_options,
1254 ),
1255 )),
1256 ..Default::default()
1257 });
1258
1259 let write_params = WriteParams {
1260 mode,
1261 store_params,
1262 session: self.session.clone(),
1263 ..Default::default()
1264 };
1265
1266 let dataset = Dataset::write(reader, table_uri, Some(write_params))
1267 .await
1268 .map_err(|e| NamespaceError::Internal {
1269 message: format!("Failed to write table at '{}': {}", table_uri, e),
1270 })?;
1271
1272 Ok(dataset)
1273 }
1274
1275 fn manifest_version_from_filename(filename: &str) -> Option<u64> {
1279 ManifestNamingScheme::detect_scheme(filename)?.parse_version(filename)
1280 }
1281
1282 async fn list_table_versions_from_storage(
1283 &self,
1284 table_uri: &str,
1285 descending: bool,
1286 limit: Option<i32>,
1287 ) -> Result<Vec<TableVersion>> {
1288 let table_path = self.object_store_path_from_uri(table_uri)?;
1289 let versions_dir = table_path.clone().join(VERSIONS_DIR);
1290 let manifest_metas: Vec<_> = self
1291 .object_store
1292 .read_dir_all(&versions_dir, None)
1293 .try_collect()
1294 .await
1295 .map_err(|e| {
1296 lance_core::Error::from(NamespaceError::Internal {
1297 message: format!(
1298 "Failed to list manifest files for table at '{}': {}",
1299 table_uri, e
1300 ),
1301 })
1302 })?;
1303
1304 let is_v2_naming = manifest_metas
1305 .first()
1306 .is_some_and(|meta| meta.location.filename().is_some_and(|f| f.len() == 29));
1307
1308 let mut table_versions: Vec<TableVersion> = manifest_metas
1309 .into_iter()
1310 .filter_map(|meta| {
1311 let filename = meta.location.filename()?;
1312 let actual_version = Self::manifest_version_from_filename(filename)?;
1313
1314 Some(TableVersion {
1315 version: actual_version as i64,
1316 manifest_path: meta.location.to_string(),
1317 manifest_size: Some(meta.size as i64),
1318 e_tag: meta.e_tag,
1319 timestamp_millis: Some(meta.last_modified.timestamp_millis()),
1320 metadata: None,
1321 })
1322 })
1323 .collect();
1324
1325 let list_is_ordered = self.object_store.list_is_lexically_ordered;
1326
1327 let needs_sort = if list_is_ordered {
1328 if is_v2_naming {
1329 !descending
1330 } else {
1331 descending
1332 }
1333 } else {
1334 true
1335 };
1336
1337 if needs_sort {
1338 if descending {
1339 table_versions.sort_by(|a, b| b.version.cmp(&a.version));
1340 } else {
1341 table_versions.sort_by(|a, b| a.version.cmp(&b.version));
1342 }
1343 }
1344
1345 if let Some(limit) = limit {
1346 table_versions.truncate(limit as usize);
1347 }
1348
1349 Ok(table_versions)
1350 }
1351
1352 async fn describe_table_impl(
1356 &self,
1357 request: DescribeTableRequest,
1358 ) -> Result<DescribeTableResponse> {
1359 let is_root_level = request.id.as_ref().is_some_and(|id| id.len() == 1);
1360 let skip_manifest_for_root = self.dir_listing_enabled
1361 && is_root_level
1362 && !self.dir_listing_to_manifest_migration_enabled;
1363 if let Some(ref manifest_ns) = self.manifest_ns
1364 && !skip_manifest_for_root
1365 {
1366 match manifest_ns.describe_table(request.clone()).await {
1367 Ok(mut response) => {
1368 if let Some(ref table_uri) = response.table_uri {
1369 let vend = request.vend_credentials.unwrap_or(true);
1371 let identity = request.identity.as_deref();
1372 response.storage_options = self
1373 .get_storage_options_for_table(table_uri, vend, identity)
1374 .await?;
1375 }
1376 if self.table_version_tracking_enabled {
1378 response.managed_versioning = Some(true);
1379 }
1380 return Ok(response);
1381 }
1382 Err(e) if manifest_feature_flags::is_incompatible_manifest_error(&e) => {
1383 return Err(e);
1386 }
1387 Err(_) if self.dir_listing_enabled && is_root_level => {
1388 }
1390 Err(e) => return Err(e),
1391 }
1392 }
1393
1394 let table_name = Self::table_name_from_id(&request.id)?;
1395 let table_id = Self::format_table_id_from_request(&request.id);
1396 let table_uri = self.table_full_uri(&table_name);
1397
1398 let status = self.check_table_status(&table_name).await;
1400
1401 if !status.exists {
1402 return Err(NamespaceError::TableNotFound {
1403 message: table_id.clone(),
1404 }
1405 .into());
1406 }
1407
1408 if status.is_deregistered {
1409 return Err(NamespaceError::TableNotFound {
1410 message: format!("Table is deregistered: {}", table_id),
1411 }
1412 .into());
1413 }
1414
1415 let load_detailed_metadata = request.load_detailed_metadata.unwrap_or(false);
1416 let should_check_declared =
1417 load_detailed_metadata || request.check_declared.unwrap_or(false);
1418 let vend_credentials = request.vend_credentials.unwrap_or(true);
1420 let identity = request.identity.as_deref();
1421 let is_only_declared = if should_check_declared {
1422 if status.has_reserved_file {
1423 Some(!self.table_has_actual_manifests(&table_name).await?)
1424 } else {
1425 Some(false)
1426 }
1427 } else {
1428 None
1429 };
1430
1431 if !load_detailed_metadata {
1432 let storage_options = self
1433 .get_storage_options_for_table(&table_uri, vend_credentials, identity)
1434 .await?;
1435 return Ok(DescribeTableResponse {
1436 table: Some(table_name),
1437 namespace: request.id.as_ref().map(|id| {
1438 if id.len() > 1 {
1439 id[..id.len() - 1].to_vec()
1440 } else {
1441 vec![]
1442 }
1443 }),
1444 location: Some(table_uri.clone()),
1445 table_uri: Some(table_uri),
1446 storage_options,
1447 is_only_declared,
1448 managed_versioning: if self.table_version_tracking_enabled {
1449 Some(true)
1450 } else {
1451 None
1452 },
1453 ..Default::default()
1454 });
1455 }
1456
1457 if is_only_declared == Some(true) {
1458 let storage_options = self
1459 .get_storage_options_for_table(&table_uri, vend_credentials, identity)
1460 .await?;
1461 return Ok(DescribeTableResponse {
1462 table: Some(table_name),
1463 namespace: request.id.as_ref().map(|id| {
1464 if id.len() > 1 {
1465 id[..id.len() - 1].to_vec()
1466 } else {
1467 vec![]
1468 }
1469 }),
1470 location: Some(table_uri.clone()),
1471 table_uri: Some(table_uri),
1472 storage_options,
1473 is_only_declared,
1474 managed_versioning: if self.table_version_tracking_enabled {
1475 Some(true)
1476 } else {
1477 None
1478 },
1479 ..Default::default()
1480 });
1481 }
1482
1483 let mut builder = DatasetBuilder::from_uri(&table_uri);
1486 if let Some(opts) = &self.storage_options {
1487 builder = builder.with_storage_options(opts.clone());
1488 }
1489 if let Some(sess) = &self.session {
1490 builder = builder.with_session(sess.clone());
1491 }
1492 match builder.load().await {
1493 Ok(mut dataset) => {
1494 if let Some(requested_version) = request.version {
1496 dataset = dataset
1497 .checkout_version(requested_version as u64)
1498 .await
1499 .map_err(|e| {
1500 lance_core::Error::from(NamespaceError::TableVersionNotFound {
1501 message: format!(
1502 "Version {} not found for table '{}': {}",
1503 requested_version, table_name, e
1504 ),
1505 })
1506 })?;
1507 }
1508
1509 let version_info = dataset.version();
1510 let lance_schema = dataset.schema();
1511 let arrow_schema: arrow_schema::Schema = lance_schema.into();
1512 let json_schema = arrow_schema_to_json(&arrow_schema)?;
1513 let storage_options = self
1514 .get_storage_options_for_table(&table_uri, vend_credentials, identity)
1515 .await?;
1516
1517 let metadata: std::collections::HashMap<String, String> =
1519 version_info.metadata.into_iter().collect();
1520
1521 Ok(DescribeTableResponse {
1522 table: Some(table_name),
1523 namespace: request.id.as_ref().map(|id| {
1524 if id.len() > 1 {
1525 id[..id.len() - 1].to_vec()
1526 } else {
1527 vec![]
1528 }
1529 }),
1530 version: Some(version_info.version as i64),
1531 location: Some(table_uri.clone()),
1532 table_uri: Some(table_uri),
1533 schema: Some(Box::new(json_schema)),
1534 storage_options,
1535 metadata: Some(metadata),
1536 is_only_declared,
1537 managed_versioning: if self.table_version_tracking_enabled {
1538 Some(true)
1539 } else {
1540 None
1541 },
1542 ..Default::default()
1543 })
1544 }
1545 Err(err) => {
1546 if manifest::ManifestNamespace::is_not_found_load_error(&err)
1547 && is_only_declared == Some(true)
1548 {
1549 let storage_options = self
1550 .get_storage_options_for_table(&table_uri, vend_credentials, identity)
1551 .await?;
1552 Ok(DescribeTableResponse {
1553 table: Some(table_name),
1554 namespace: request.id.as_ref().map(|id| {
1555 if id.len() > 1 {
1556 id[..id.len() - 1].to_vec()
1557 } else {
1558 vec![]
1559 }
1560 }),
1561 location: Some(table_uri.clone()),
1562 table_uri: Some(table_uri),
1563 storage_options,
1564 is_only_declared,
1565 managed_versioning: if self.table_version_tracking_enabled {
1566 Some(true)
1567 } else {
1568 None
1569 },
1570 ..Default::default()
1571 })
1572 } else {
1573 Err(NamespaceError::Internal {
1574 message: format!(
1575 "Table directory exists but cannot load dataset {}: {:?}",
1576 table_name, err
1577 ),
1578 }
1579 .into())
1580 }
1581 }
1582 }
1583 }
1584
1585 fn configured_builder(&self, table_uri: &str) -> DatasetBuilder {
1588 let mut builder = DatasetBuilder::from_uri(table_uri);
1589 if let Some(opts) = &self.storage_options {
1590 builder = builder.with_storage_options(opts.clone());
1591 }
1592 if let Some(sess) = &self.session {
1593 builder = builder.with_session(sess.clone());
1594 }
1595 builder
1596 }
1597
1598 async fn load_dataset(
1599 &self,
1600 table_uri: &str,
1601 version: Option<i64>,
1602 operation: &str,
1603 ) -> Result<Dataset> {
1604 if let Some(version) = version
1605 && version < 0
1606 {
1607 return Err(NamespaceError::InvalidInput {
1608 message: format!(
1609 "Table version for {} must be non-negative, got {}",
1610 operation, version
1611 ),
1612 }
1613 .into());
1614 }
1615
1616 let builder = self.configured_builder(table_uri);
1617
1618 let dataset = builder.load().await.map_err(|e| {
1619 lance_core::Error::from(NamespaceError::TableNotFound {
1620 message: format!(
1621 "Failed to open table at '{}' for {}: {}",
1622 table_uri, operation, e
1623 ),
1624 })
1625 })?;
1626
1627 if let Some(version) = version {
1628 return dataset.checkout_version(version as u64).await.map_err(|e| {
1629 lance_core::Error::from(NamespaceError::TableVersionNotFound {
1630 message: format!(
1631 "Failed to checkout version {} for table at '{}' during {}: {}",
1632 version, table_uri, operation, e
1633 ),
1634 })
1635 });
1636 }
1637
1638 Ok(dataset)
1639 }
1640
1641 fn parse_index_type(index_type: &str) -> Result<IndexType> {
1642 match index_type.trim().to_ascii_uppercase().as_str() {
1643 "SCALAR" | "BTREE" => Ok(IndexType::BTree),
1644 "BITMAP" => Ok(IndexType::Bitmap),
1645 "LABEL_LIST" | "LABELLIST" => Ok(IndexType::LabelList),
1646 "INVERTED" | "FTS" => Ok(IndexType::Inverted),
1647 "NGRAM" => Ok(IndexType::NGram),
1648 "ZONEMAP" | "ZONE_MAP" => Ok(IndexType::ZoneMap),
1649 "BLOOMFILTER" | "BLOOM_FILTER" => Ok(IndexType::BloomFilter),
1650 "RTREE" | "R_TREE" => Ok(IndexType::RTree),
1651 "VECTOR" | "IVF_PQ" => Ok(IndexType::IvfPq),
1652 "IVF_FLAT" => Ok(IndexType::IvfFlat),
1653 "IVF_SQ" => Ok(IndexType::IvfSq),
1654 "IVF_RQ" => Ok(IndexType::IvfRq),
1655 "IVF_HNSW_FLAT" => Ok(IndexType::IvfHnswFlat),
1656 "IVF_HNSW_SQ" => Ok(IndexType::IvfHnswSq),
1657 "IVF_HNSW_PQ" => Ok(IndexType::IvfHnswPq),
1658 other => Err(NamespaceError::InvalidInput {
1659 message: format!("Unsupported index_type '{}'", other),
1660 }
1661 .into()),
1662 }
1663 }
1664
1665 fn parse_metric_type(distance_type: Option<&str>) -> Result<MetricType> {
1666 let distance_type = distance_type.unwrap_or("l2");
1667 MetricType::try_from(distance_type).map_err(|e| {
1668 lance_core::Error::from(NamespaceError::InvalidInput {
1669 message: format!(
1670 "Unsupported distance_type '{}' for vector index: {}",
1671 distance_type, e
1672 ),
1673 })
1674 })
1675 }
1676
1677 fn build_index_params(request: &CreateTableIndexRequest) -> Result<DirectoryIndexParams> {
1678 let index_type = Self::parse_index_type(&request.index_type)?;
1679 Ok(match index_type {
1680 IndexType::BTree => DirectoryIndexParams::Scalar {
1681 index_type,
1682 params: ScalarIndexParams::for_builtin(BuiltinIndexType::BTree),
1683 },
1684 IndexType::Bitmap => DirectoryIndexParams::Scalar {
1685 index_type,
1686 params: ScalarIndexParams::for_builtin(BuiltinIndexType::Bitmap),
1687 },
1688 IndexType::LabelList => DirectoryIndexParams::Scalar {
1689 index_type,
1690 params: ScalarIndexParams::for_builtin(BuiltinIndexType::LabelList),
1691 },
1692 IndexType::NGram => DirectoryIndexParams::Scalar {
1693 index_type,
1694 params: ScalarIndexParams::for_builtin(BuiltinIndexType::NGram),
1695 },
1696 IndexType::ZoneMap => DirectoryIndexParams::Scalar {
1697 index_type,
1698 params: ScalarIndexParams::for_builtin(BuiltinIndexType::ZoneMap),
1699 },
1700 IndexType::BloomFilter => DirectoryIndexParams::Scalar {
1701 index_type,
1702 params: ScalarIndexParams::for_builtin(BuiltinIndexType::BloomFilter),
1703 },
1704 IndexType::RTree => DirectoryIndexParams::Scalar {
1705 index_type,
1706 params: ScalarIndexParams::for_builtin(BuiltinIndexType::RTree),
1707 },
1708 IndexType::Inverted => {
1709 let mut params = InvertedIndexParams::default();
1710 if let Some(with_position) = request.with_position {
1711 params = params.with_position(with_position);
1712 }
1713 if let Some(base_tokenizer) = &request.base_tokenizer {
1714 params = params.base_tokenizer(base_tokenizer.clone());
1715 }
1716 if let Some(language) = &request.language {
1717 params = params.language(language)?;
1718 }
1719 if let Some(max_token_length) = request.max_token_length {
1720 if max_token_length < 0 {
1721 return Err(NamespaceError::InvalidInput {
1722 message: format!(
1723 "FTS max_token_length must be non-negative, got {}",
1724 max_token_length
1725 ),
1726 }
1727 .into());
1728 }
1729 params = params.max_token_length(Some(max_token_length as usize));
1730 }
1731 if let Some(lower_case) = request.lower_case {
1732 params = params.lower_case(lower_case);
1733 }
1734 if let Some(stem) = request.stem {
1735 params = params.stem(stem);
1736 }
1737 if let Some(remove_stop_words) = request.remove_stop_words {
1738 params = params.remove_stop_words(remove_stop_words);
1739 }
1740 if let Some(ascii_folding) = request.ascii_folding {
1741 params = params.ascii_folding(ascii_folding);
1742 }
1743 DirectoryIndexParams::Inverted(params)
1744 }
1745 IndexType::IvfFlat => DirectoryIndexParams::Vector {
1746 index_type,
1747 params: VectorIndexParams::with_ivf_flat_params(
1748 Self::parse_metric_type(request.distance_type.as_deref())?,
1749 IvfBuildParams::default(),
1750 ),
1751 },
1752 IndexType::IvfPq => DirectoryIndexParams::Vector {
1753 index_type,
1754 params: VectorIndexParams::with_ivf_pq_params(
1755 Self::parse_metric_type(request.distance_type.as_deref())?,
1756 IvfBuildParams::default(),
1757 PQBuildParams::default(),
1758 ),
1759 },
1760 IndexType::IvfSq => DirectoryIndexParams::Vector {
1761 index_type,
1762 params: VectorIndexParams::with_ivf_sq_params(
1763 Self::parse_metric_type(request.distance_type.as_deref())?,
1764 IvfBuildParams::default(),
1765 SQBuildParams::default(),
1766 ),
1767 },
1768 IndexType::IvfRq => DirectoryIndexParams::Vector {
1769 index_type,
1770 params: VectorIndexParams::with_ivf_rq_params(
1771 Self::parse_metric_type(request.distance_type.as_deref())?,
1772 IvfBuildParams::default(),
1773 RQBuildParams::default(),
1774 ),
1775 },
1776 IndexType::IvfHnswFlat => DirectoryIndexParams::Vector {
1777 index_type,
1778 params: VectorIndexParams::ivf_hnsw(
1779 Self::parse_metric_type(request.distance_type.as_deref())?,
1780 IvfBuildParams::default(),
1781 HnswBuildParams::default(),
1782 ),
1783 },
1784 IndexType::IvfHnswSq => DirectoryIndexParams::Vector {
1785 index_type,
1786 params: VectorIndexParams::with_ivf_hnsw_sq_params(
1787 Self::parse_metric_type(request.distance_type.as_deref())?,
1788 IvfBuildParams::default(),
1789 HnswBuildParams::default(),
1790 SQBuildParams::default(),
1791 ),
1792 },
1793 IndexType::IvfHnswPq => DirectoryIndexParams::Vector {
1794 index_type,
1795 params: VectorIndexParams::with_ivf_hnsw_pq_params(
1796 Self::parse_metric_type(request.distance_type.as_deref())?,
1797 IvfBuildParams::default(),
1798 HnswBuildParams::default(),
1799 PQBuildParams::default(),
1800 ),
1801 },
1802 other => {
1803 return Err(NamespaceError::InvalidInput {
1804 message: format!("Unsupported index type for namespace API: {}", other),
1805 }
1806 .into());
1807 }
1808 })
1809 }
1810
1811 fn paginate_indices(
1812 indices: &mut Vec<IndexContent>,
1813 page_token: Option<String>,
1814 limit: Option<i32>,
1815 ) -> Option<String> {
1816 indices.sort_by(|a, b| a.index_name.cmp(&b.index_name));
1817
1818 if let Some(start_after) = page_token {
1819 if let Some(index) = indices
1820 .iter()
1821 .position(|index| index.index_name.as_str() > start_after.as_str())
1822 {
1823 indices.drain(0..index);
1824 } else {
1825 indices.clear();
1826 }
1827 }
1828
1829 let mut next_page_token = None;
1830 if let Some(limit) = limit
1831 && limit >= 0
1832 {
1833 let limit = limit as usize;
1834 if limit > 0 && indices.len() > limit {
1835 next_page_token = Some(indices[limit - 1].index_name.clone());
1836 }
1837 indices.truncate(limit);
1838 }
1839 if indices.is_empty() {
1840 None
1841 } else {
1842 next_page_token
1843 }
1844 }
1845
1846 fn transaction_operation_name(transaction: &Transaction) -> String {
1847 match &transaction.operation {
1848 Operation::CreateIndex {
1849 new_indices,
1850 removed_indices,
1851 } if new_indices.is_empty() && !removed_indices.is_empty() => "DropIndex".to_string(),
1852 _ => transaction.operation.to_string(),
1853 }
1854 }
1855
1856 fn transaction_response(
1857 version: u64,
1858 transaction: &Transaction,
1859 ) -> DescribeTransactionResponse {
1860 let mut properties = transaction
1861 .transaction_properties
1862 .as_ref()
1863 .map(|properties| (**properties).clone())
1864 .unwrap_or_default();
1865 properties.insert("uuid".to_string(), transaction.uuid.clone());
1866 properties.insert("version".to_string(), version.to_string());
1867 properties.insert(
1868 "read_version".to_string(),
1869 transaction.read_version.to_string(),
1870 );
1871 properties.insert(
1872 "operation".to_string(),
1873 Self::transaction_operation_name(transaction),
1874 );
1875 if let Some(tag) = &transaction.tag {
1876 properties.insert("tag".to_string(), tag.clone());
1877 }
1878
1879 DescribeTransactionResponse {
1880 status: "SUCCEEDED".to_string(),
1881 properties: Some(properties),
1882 }
1883 }
1884
1885 fn describe_table_index_stats_response(
1886 stats: &serde_json::Value,
1887 ) -> DescribeTableIndexStatsResponse {
1888 let get_i64 = |key: &str| {
1889 stats.get(key).and_then(|value| {
1890 value
1891 .as_i64()
1892 .or_else(|| value.as_u64().and_then(|v| i64::try_from(v).ok()))
1893 })
1894 };
1895
1896 DescribeTableIndexStatsResponse {
1897 distance_type: stats
1898 .get("distance_type")
1899 .and_then(|value| value.as_str())
1900 .map(str::to_string),
1901 index_type: stats
1902 .get("index_type")
1903 .and_then(|value| value.as_str())
1904 .map(str::to_string),
1905 num_indexed_rows: get_i64("num_indexed_rows"),
1906 num_unindexed_rows: get_i64("num_unindexed_rows"),
1907 num_indices: get_i64("num_indices").and_then(|value| i32::try_from(value).ok()),
1908 }
1909 }
1910
1911 async fn find_transaction(&self, dataset: &Dataset, id: &str) -> Result<(u64, Transaction)> {
1916 if let Ok(version) = id.parse::<u64>() {
1917 let transaction = dataset
1918 .read_transaction_by_version(version)
1919 .await
1920 .map_err(|e| {
1921 lance_core::Error::from(NamespaceError::TransactionNotFound {
1922 message: format!(
1923 "Failed to read transaction for version {}: {}",
1924 version, e
1925 ),
1926 })
1927 })?
1928 .ok_or_else(|| {
1929 lance_core::Error::from(NamespaceError::TransactionNotFound {
1930 message: format!("version {}", version),
1931 })
1932 })?;
1933 return Ok((version, transaction));
1934 }
1935
1936 let versions = dataset.versions().await.map_err(|e| {
1937 lance_core::Error::from(NamespaceError::Internal {
1938 message: format!(
1939 "Failed to list table versions while resolving transaction '{}': {}",
1940 id, e
1941 ),
1942 })
1943 })?;
1944
1945 for version in versions.into_iter().rev() {
1946 if let Some(transaction) = dataset
1947 .read_transaction_by_version(version.version)
1948 .await
1949 .map_err(|e| {
1950 lance_core::Error::from(NamespaceError::Internal {
1951 message: format!(
1952 "Failed to read transaction for version {} while resolving '{}': {}",
1953 version.version, id, e
1954 ),
1955 })
1956 })?
1957 && transaction.uuid == id
1958 {
1959 return Ok((version.version, transaction));
1960 }
1961 }
1962
1963 Err(NamespaceError::TransactionNotFound {
1964 message: id.to_string(),
1965 }
1966 .into())
1967 }
1968
1969 fn table_full_uri(&self, table_name: &str) -> String {
1970 format!("{}/{}.lance", &self.root, table_name)
1971 }
1972
1973 fn table_path(&self, table_name: &str) -> Path {
1975 self.base_path
1976 .clone()
1977 .join(format!("{}.lance", table_name).as_str())
1978 }
1979
1980 fn table_reserved_file_path(&self, table_name: &str) -> Path {
1982 self.base_path
1983 .clone()
1984 .join(format!("{}.lance", table_name).as_str())
1985 .join(".lance-reserved")
1986 }
1987
1988 fn table_deregistered_file_path(&self, table_name: &str) -> Path {
1990 self.base_path
1991 .clone()
1992 .join(format!("{}.lance", table_name).as_str())
1993 .join(".lance-deregistered")
1994 }
1995
1996 pub(crate) async fn check_table_status(&self, table_name: &str) -> TableStatus {
2002 let table_path = self.table_path(table_name);
2003 match self.object_store.read_dir(table_path).await {
2004 Ok(entries) => {
2005 let exists = !entries.is_empty();
2006 let is_deregistered = entries.iter().any(|e| e.ends_with(".lance-deregistered"));
2007 let has_reserved_file = entries.iter().any(|e| e.ends_with(".lance-reserved"));
2008 TableStatus {
2009 exists,
2010 is_deregistered,
2011 has_reserved_file,
2012 }
2013 }
2014 Err(_) => TableStatus {
2015 exists: false,
2016 is_deregistered: false,
2017 has_reserved_file: false,
2018 },
2019 }
2020 }
2021
2022 async fn put_marker_file_atomic(
2023 &self,
2024 path: &Path,
2025 file_description: &str,
2026 ) -> std::result::Result<(), String> {
2027 let put_opts = PutOptions {
2028 mode: PutMode::Create,
2029 ..Default::default()
2030 };
2031
2032 match self
2033 .object_store
2034 .inner
2035 .put_opts(path, bytes::Bytes::new().into(), put_opts)
2036 .await
2037 {
2038 Ok(_) => Ok(()),
2039 Err(ObjectStoreError::AlreadyExists { .. })
2040 | Err(ObjectStoreError::Precondition { .. }) => {
2041 Err(format!("{} already exists", file_description))
2042 }
2043 Err(e) => Err(format!("Failed to create {}: {:?}", file_description, e)),
2044 }
2045 }
2046
2047 async fn get_storage_options_for_table(
2067 &self,
2068 table_uri: &str,
2069 vend_credentials: bool,
2070 identity: Option<&Identity>,
2071 ) -> Result<Option<HashMap<String, String>>> {
2072 if vend_credentials && let Some(ref vendor) = self.credential_vendor {
2073 let vended = vendor.vend_credentials(table_uri, identity).await?;
2074 return Ok(Some(vended.storage_options));
2075 }
2076 if self.vend_input_storage_options {
2079 let mut options = self.storage_options.clone().unwrap_or_default();
2080 if let Some(refresh_interval_millis) =
2082 self.vend_input_storage_options_refresh_interval_millis
2083 {
2084 let now_millis = std::time::SystemTime::now()
2085 .duration_since(std::time::UNIX_EPOCH)
2086 .unwrap()
2087 .as_millis() as u64;
2088 let expires_at_millis = now_millis + refresh_interval_millis;
2089 options.insert(
2090 "expires_at_millis".to_string(),
2091 expires_at_millis.to_string(),
2092 );
2093 }
2094 return Ok(Some(options));
2095 }
2096 Ok(None)
2099 }
2100
2101 pub async fn migrate(&self) -> Result<usize> {
2155 let Some(ref manifest_ns) = self.manifest_ns else {
2157 return Ok(0); };
2159
2160 let manifest_locations = manifest_ns.list_manifest_table_locations().await?;
2162
2163 let dir_tables = self
2166 .filter_declared_tables(self.list_directory_tables().await?, false)
2167 .await?;
2168
2169 let mut migrated_count = 0;
2174 for table_name in dir_tables {
2175 let dir_name = format!("{}.lance", table_name);
2177 if !manifest_locations.contains(&dir_name) {
2178 manifest_ns.register_table(&table_name, dir_name).await?;
2179 migrated_count += 1;
2180 }
2181 }
2182
2183 Ok(migrated_count)
2184 }
2185
2186 async fn delete_physical_version_files(
2194 &self,
2195 table_entries: &[TableDeleteEntry],
2196 branch: Option<&str>,
2197 ) -> Result<i64> {
2198 let mut deleted_count = 0i64;
2199 for te in table_entries {
2200 let table_uri = self.resolve_table_location(&te.table_id).await?;
2201 let table_uri = match branch {
2202 Some(b) => self.resolve_branch_location(&table_uri, b).await?,
2203 None => table_uri,
2204 };
2205 let table_path = self.object_store_path_from_uri(&table_uri)?;
2206 let versions_dir_path = table_path.clone().join(VERSIONS_DIR);
2207
2208 let manifest_metas: Vec<_> = self
2210 .object_store
2211 .read_dir_all(&versions_dir_path, None)
2212 .try_collect()
2213 .await
2214 .map_err(|e| {
2215 lance_core::Error::from(NamespaceError::Internal {
2216 message: format!(
2217 "Failed to list manifest files for table at '{}': {}",
2218 table_uri, e
2219 ),
2220 })
2221 })?;
2222 let location_by_version: HashMap<u64, Path> = manifest_metas
2223 .into_iter()
2224 .filter_map(|meta| {
2225 let version = Self::manifest_version_from_filename(meta.location.filename()?)?;
2226 Some((version, meta.location))
2227 })
2228 .collect();
2229
2230 for (&v, version_path) in &location_by_version {
2231 let vi = v as i64;
2232 if !te.ranges.iter().any(|&(s, e)| vi >= s && (e < 0 || vi < e)) {
2233 continue;
2234 }
2235 match self.object_store.inner.delete(version_path).await {
2236 Ok(_) => {
2237 deleted_count += 1;
2238 }
2239 Err(object_store::Error::NotFound { .. }) => {}
2240 Err(e) => {
2241 return Err(NamespaceError::Internal {
2242 message: format!(
2243 "Failed to delete version {} for table at '{}': {}",
2244 v, table_uri, e
2245 ),
2246 }
2247 .into());
2248 }
2249 }
2250 }
2251 }
2252 Ok(deleted_count)
2253 }
2254
2255 #[allow(clippy::too_many_arguments)]
2260 fn apply_query_params_to_scanner(
2261 scanner: &mut Scanner,
2262 filter: Option<&str>,
2263 columns: Option<&QueryTableRequestColumns>,
2264 vector_column: Option<&str>,
2265 vector: &QueryTableRequestVector,
2266 k: i32,
2267 offset: Option<i32>,
2268 prefilter: Option<bool>,
2269 bypass_vector_index: Option<bool>,
2270 nprobes: Option<i32>,
2271 ef: Option<i32>,
2272 refine_factor: Option<i32>,
2273 distance_type: Option<&str>,
2274 fast_search_flag: Option<bool>,
2275 with_row_id: Option<bool>,
2276 lower_bound: Option<f32>,
2277 upper_bound: Option<f32>,
2278 operation: &str,
2279 ) -> Result<()> {
2280 if let Some(pf) = prefilter {
2282 scanner.prefilter(pf);
2283 }
2284
2285 if let Some(filter) = filter {
2286 scanner.filter(filter).map_err(|e| {
2287 Error::invalid_input_source(
2288 format!("Invalid filter expression for {}: {}", operation, e).into(),
2289 )
2290 })?;
2291 }
2292
2293 if let Some(cols) = columns {
2294 if let Some(ref names) = cols.column_names {
2295 scanner.project(names.as_slice()).map_err(|e| {
2296 Error::invalid_input_source(
2297 format!("Invalid column projection for {}: {}", operation, e).into(),
2298 )
2299 })?;
2300 } else if let Some(ref aliases) = cols.column_aliases {
2301 let pairs: Vec<(&str, &str)> = aliases
2303 .iter()
2304 .map(|(alias, src)| (alias.as_str(), src.as_str()))
2305 .collect();
2306 scanner.project_with_transform(&pairs).map_err(|e| {
2307 Error::invalid_input_source(
2308 format!("Invalid column aliases for {}: {}", operation, e).into(),
2309 )
2310 })?;
2311 }
2312 }
2313
2314 let query_vec: Option<Vec<f32>> = vector
2316 .single_vector
2317 .as_ref()
2318 .filter(|v| !v.is_empty())
2319 .cloned()
2320 .or_else(|| {
2321 vector
2322 .multi_vector
2323 .as_ref()
2324 .and_then(|mv| mv.first())
2325 .filter(|v| !v.is_empty())
2326 .cloned()
2327 });
2328
2329 if let Some(q_vec) = query_vec {
2330 let col = vector_column.unwrap_or("vector");
2331 let q = Arc::new(Float32Array::from(q_vec));
2332 scanner
2333 .nearest(col, q.as_ref(), k.max(1) as usize)
2334 .map_err(|e| {
2335 Error::invalid_input_source(
2336 format!("Invalid vector query for {}: {}", operation, e).into(),
2337 )
2338 })?;
2339
2340 if let Some(n) = nprobes {
2342 scanner.nprobes(n.max(1) as usize);
2343 }
2344 if let Some(e) = ef {
2345 scanner.ef(e.max(1) as usize);
2346 }
2347 if let Some(rf) = refine_factor {
2348 scanner.refine(rf.max(0) as u32);
2349 }
2350 if let Some(true) = bypass_vector_index {
2352 scanner.use_index(false);
2353 }
2354 if let Some(true) = fast_search_flag {
2355 scanner.fast_search();
2356 }
2357 if lower_bound.is_some() || upper_bound.is_some() {
2358 scanner.distance_range(lower_bound, upper_bound);
2359 }
2360 if let Some(dt) = distance_type {
2361 let metric = Self::parse_metric_type(Some(dt))?;
2362 scanner.distance_metric(metric);
2363 }
2364 if let Some(off) = offset.filter(|&o| o > 0) {
2366 scanner.limit(None, Some(off as i64)).map_err(|e| {
2367 Error::invalid_input_source(
2368 format!("Invalid offset for {}: {}", operation, e).into(),
2369 )
2370 })?;
2371 }
2372 } else {
2373 let limit = if k > 0 { Some(k as i64) } else { None };
2375 scanner
2376 .limit(limit, offset.map(|o| o as i64))
2377 .map_err(|e| {
2378 Error::invalid_input_source(
2379 format!("Invalid limit/offset for {}: {}", operation, e).into(),
2380 )
2381 })?;
2382 }
2383
2384 if let Some(true) = with_row_id {
2385 scanner.with_row_id();
2386 }
2387
2388 Ok(())
2389 }
2390
2391 pub fn retrieve_ops_metrics(&self) -> HashMap<String, u64> {
2398 self.ops_metrics
2399 .as_ref()
2400 .map(|m| m.retrieve())
2401 .unwrap_or_default()
2402 }
2403
2404 pub fn reset_ops_metrics(&self) {
2408 if let Some(ref metrics) = self.ops_metrics {
2409 metrics.reset();
2410 }
2411 }
2412
2413 fn record_op(&self, operation: &str) {
2415 if let Some(ref metrics) = self.ops_metrics {
2416 metrics.increment(operation);
2417 }
2418 }
2419}
2420
2421#[async_trait]
2422impl LanceNamespace for DirectoryNamespace {
2423 async fn list_namespaces(
2424 &self,
2425 request: ListNamespacesRequest,
2426 ) -> Result<ListNamespacesResponse> {
2427 self.record_op("list_namespaces");
2428 if let Some(ref manifest_ns) = self.manifest_ns {
2429 return manifest_ns.list_namespaces(request).await;
2430 }
2431
2432 Self::validate_root_namespace_id(&request.id)?;
2433 Ok(ListNamespacesResponse::new(vec![]))
2434 }
2435
2436 async fn describe_namespace(
2437 &self,
2438 request: DescribeNamespaceRequest,
2439 ) -> Result<DescribeNamespaceResponse> {
2440 self.record_op("describe_namespace");
2441 if let Some(ref manifest_ns) = self.manifest_ns {
2442 return manifest_ns.describe_namespace(request).await;
2443 }
2444
2445 Self::validate_root_namespace_id(&request.id)?;
2446 #[allow(clippy::needless_update)]
2447 Ok(DescribeNamespaceResponse {
2448 properties: Some(HashMap::new()),
2449 ..Default::default()
2450 })
2451 }
2452
2453 async fn create_namespace(
2454 &self,
2455 request: CreateNamespaceRequest,
2456 ) -> Result<CreateNamespaceResponse> {
2457 self.record_op("create_namespace");
2458 if let Some(ref manifest_ns) = self.manifest_ns {
2459 return manifest_ns.create_namespace(request).await;
2460 }
2461
2462 if request.id.is_none() || request.id.as_ref().unwrap().is_empty() {
2463 return Err(NamespaceError::NamespaceAlreadyExists {
2464 message: "root namespace".to_string(),
2465 }
2466 .into());
2467 }
2468
2469 Err(NamespaceError::Unsupported {
2470 message: "Child namespaces are only supported when manifest mode is enabled"
2471 .to_string(),
2472 }
2473 .into())
2474 }
2475
2476 async fn drop_namespace(&self, request: DropNamespaceRequest) -> Result<DropNamespaceResponse> {
2477 self.record_op("drop_namespace");
2478 if let Some(ref manifest_ns) = self.manifest_ns {
2479 return manifest_ns.drop_namespace(request).await;
2480 }
2481
2482 if request.id.is_none() || request.id.as_ref().unwrap().is_empty() {
2483 return Err(NamespaceError::InvalidInput {
2484 message: "Root namespace cannot be dropped".to_string(),
2485 }
2486 .into());
2487 }
2488
2489 Err(NamespaceError::Unsupported {
2490 message: "Child namespaces are only supported when manifest mode is enabled"
2491 .to_string(),
2492 }
2493 .into())
2494 }
2495
2496 async fn namespace_exists(&self, request: NamespaceExistsRequest) -> Result<()> {
2497 self.record_op("namespace_exists");
2498 if let Some(ref manifest_ns) = self.manifest_ns {
2499 return manifest_ns.namespace_exists(request).await;
2500 }
2501
2502 if request.id.is_none() || request.id.as_ref().unwrap().is_empty() {
2503 return Ok(());
2504 }
2505
2506 Err(NamespaceError::NamespaceNotFound {
2507 message: "Child namespaces are only supported when manifest mode is enabled"
2508 .to_string(),
2509 }
2510 .into())
2511 }
2512
2513 async fn list_tables(&self, request: ListTablesRequest) -> Result<ListTablesResponse> {
2514 self.record_op("list_tables");
2515 let namespace_id = request.id.as_ref().ok_or_else(|| {
2517 lance_core::Error::from(NamespaceError::InvalidInput {
2518 message: "Namespace ID is required".to_string(),
2519 })
2520 })?;
2521
2522 if !namespace_id.is_empty() {
2524 if let Some(ref manifest_ns) = self.manifest_ns {
2525 return manifest_ns.list_tables(request).await;
2526 }
2527 return Err(NamespaceError::Unsupported {
2528 message: "Child namespaces are only supported when manifest mode is enabled"
2529 .to_string(),
2530 }
2531 .into());
2532 }
2533
2534 if let Some(ref manifest_ns) = self.manifest_ns
2536 && !self.dir_listing_enabled
2537 {
2538 return manifest_ns.list_tables(request).await;
2539 }
2540
2541 let mut tables = if self.manifest_ns.is_some()
2544 && self.dir_listing_enabled
2545 && self.dir_listing_to_manifest_migration_enabled
2546 {
2547 let manifest_locations = if let Some(ref manifest_ns) = self.manifest_ns {
2549 manifest_ns.list_manifest_table_locations().await?
2550 } else {
2551 std::collections::HashSet::new()
2552 };
2553
2554 let mut manifest_request = request.clone();
2556 manifest_request.limit = None;
2557 manifest_request.page_token = None;
2558 let manifest_tables = if let Some(ref manifest_ns) = self.manifest_ns {
2559 let manifest_response = manifest_ns.list_tables(manifest_request).await?;
2560 manifest_response.tables
2561 } else {
2562 vec![]
2563 };
2564
2565 let mut all_tables: Vec<String> = manifest_tables;
2568 let dir_tables = self.list_directory_tables().await?;
2569 for table_name in dir_tables {
2570 let full_location = format!("{}/{}.lance", self.root, table_name);
2573 let relative_location = format!("{}.lance", table_name);
2574 if !manifest_locations.contains(&full_location)
2575 && !manifest_locations.contains(&relative_location)
2576 {
2577 all_tables.push(table_name);
2578 }
2579 }
2580
2581 all_tables
2582 } else {
2583 self.list_directory_tables().await?
2584 };
2585
2586 tables = self
2587 .filter_declared_tables(tables, request.include_declared.unwrap_or(true))
2588 .await?;
2589
2590 let next_page_token =
2592 Self::apply_pagination(&mut tables, request.page_token, request.limit);
2593 let mut response = ListTablesResponse::new(tables);
2594 response.page_token = next_page_token;
2595 Ok(response)
2596 }
2597
2598 async fn describe_table(&self, request: DescribeTableRequest) -> Result<DescribeTableResponse> {
2599 self.record_op("describe_table");
2600 self.describe_table_impl(request).await
2601 }
2602
2603 async fn table_exists(&self, request: TableExistsRequest) -> Result<()> {
2604 self.record_op("table_exists");
2605 let is_root_level = request.id.as_ref().is_some_and(|id| id.len() == 1);
2606 let skip_manifest_for_root = self.dir_listing_enabled
2607 && is_root_level
2608 && !self.dir_listing_to_manifest_migration_enabled;
2609 if let Some(ref manifest_ns) = self.manifest_ns
2610 && !skip_manifest_for_root
2611 {
2612 match manifest_ns.table_exists(request.clone()).await {
2613 Ok(()) => return Ok(()),
2614 Err(e) if manifest_feature_flags::is_incompatible_manifest_error(&e) => {
2615 return Err(e);
2618 }
2619 Err(_) if self.dir_listing_enabled && is_root_level => {
2620 }
2622 Err(e) => return Err(e),
2623 }
2624 }
2625
2626 let table_name = Self::table_name_from_id(&request.id)?;
2627 let table_id = Self::format_table_id_from_request(&request.id);
2628
2629 let status = self.check_table_status(&table_name).await;
2631
2632 if !status.exists {
2633 return Err(NamespaceError::TableNotFound {
2634 message: table_id.clone(),
2635 }
2636 .into());
2637 }
2638
2639 if status.is_deregistered {
2640 return Err(NamespaceError::TableNotFound {
2641 message: format!("Table is deregistered: {}", table_id),
2642 }
2643 .into());
2644 }
2645
2646 Ok(())
2647 }
2648
2649 async fn drop_table(&self, request: DropTableRequest) -> Result<DropTableResponse> {
2650 self.record_op("drop_table");
2651 if let Some(ref manifest_ns) = self.manifest_ns {
2652 return manifest_ns.drop_table(request).await;
2653 }
2654
2655 let table_name = Self::table_name_from_id(&request.id)?;
2656 let table_uri = self.table_full_uri(&table_name);
2657 let table_path = self.table_path(&table_name);
2658
2659 self.object_store
2660 .remove_dir_all(table_path)
2661 .await
2662 .map_err(|e| {
2663 lance_core::Error::from(NamespaceError::Internal {
2664 message: format!("Failed to drop table {}: {:?}", table_name, e),
2665 })
2666 })?;
2667
2668 Ok(DropTableResponse {
2669 id: request.id,
2670 location: Some(table_uri),
2671 ..Default::default()
2672 })
2673 }
2674
2675 async fn create_table(
2676 &self,
2677 request: CreateTableRequest,
2678 request_data: Bytes,
2679 ) -> Result<CreateTableResponse> {
2680 self.record_op("create_table");
2681 if let Some(ref manifest_ns) = self.manifest_ns {
2682 return manifest_ns.create_table(request, request_data).await;
2683 }
2684
2685 Self::validate_dir_only_properties(request.properties.as_ref(), "create_table")?;
2686
2687 let table_name = Self::table_name_from_id(&request.id)?;
2688 let table_uri = self.table_full_uri(&table_name);
2689 let status = self.check_table_status(&table_name).await;
2690 let (reader, _num_rows) =
2691 Self::ipc_reader_from_request_data(&request_data, "create_table")?;
2692
2693 if status.exists && self.table_has_actual_manifests(&table_name).await? {
2694 return Err(NamespaceError::TableAlreadyExists {
2695 message: table_name,
2696 }
2697 .into());
2698 }
2699
2700 let write_result = self
2701 .write_reader_to_table(
2702 &table_uri,
2703 reader,
2704 WriteMode::Create,
2705 request.storage_options.clone(),
2706 )
2707 .await;
2708 if let Err(err) = write_result {
2709 if self.table_uri_has_actual_manifests(&table_uri).await? {
2710 return Err(NamespaceError::TableAlreadyExists {
2711 message: table_name,
2712 }
2713 .into());
2714 }
2715 return Err(err);
2716 }
2717 Ok(CreateTableResponse {
2718 version: Some(1),
2719 location: Some(table_uri),
2720 storage_options: self.storage_options.clone(),
2721 properties: request.properties,
2722 ..Default::default()
2723 })
2724 }
2725
2726 async fn declare_table(&self, request: DeclareTableRequest) -> Result<DeclareTableResponse> {
2727 self.record_op("declare_table");
2728 if let Some(ref manifest_ns) = self.manifest_ns {
2729 let mut response = manifest_ns.declare_table(request.clone()).await?;
2730 if let Some(ref location) = response.location {
2731 let vend = request.vend_credentials.unwrap_or(true);
2733 let identity = request.identity.as_deref();
2734 response.storage_options = self
2735 .get_storage_options_for_table(location, vend, identity)
2736 .await?;
2737 }
2738 if self.table_version_tracking_enabled {
2740 response.managed_versioning = Some(true);
2741 }
2742 return Ok(response);
2743 }
2744
2745 Self::validate_dir_only_properties(request.properties.as_ref(), "declare_table")?;
2746
2747 let table_name = Self::table_name_from_id(&request.id)?;
2748 let table_uri = self.table_full_uri(&table_name);
2749
2750 if let Some(location) = &request.location {
2752 let location = location.trim_end_matches('/');
2753 if location != table_uri {
2754 return Err(NamespaceError::InvalidInput {
2755 message: format!(
2756 "Cannot declare table {} at location {}, must be at location {}",
2757 table_name, location, table_uri
2758 ),
2759 }
2760 .into());
2761 }
2762 }
2763
2764 let status = self.check_table_status(&table_name).await;
2768 if status.exists && !status.has_reserved_file {
2769 return Err(NamespaceError::TableAlreadyExists {
2771 message: table_name.to_string(),
2772 }
2773 .into());
2774 }
2775
2776 let reserved_file_path = self.table_reserved_file_path(&table_name);
2780
2781 self.put_marker_file_atomic(&reserved_file_path, &format!("table {}", table_name))
2782 .await
2783 .map_err(|e| {
2784 if e.contains("already exists") {
2785 lance_core::Error::from(NamespaceError::TableAlreadyExists {
2786 message: table_name.to_string(),
2787 })
2788 } else {
2789 lance_core::Error::from(NamespaceError::Internal { message: e })
2790 }
2791 })?;
2792
2793 let vend_credentials = request.vend_credentials.unwrap_or(true);
2795 let identity = request.identity.as_deref();
2796 let storage_options = self
2797 .get_storage_options_for_table(&table_uri, vend_credentials, identity)
2798 .await?;
2799
2800 Ok(DeclareTableResponse {
2801 location: Some(table_uri),
2802 storage_options,
2803 properties: request.properties,
2804 managed_versioning: if self.table_version_tracking_enabled {
2805 Some(true)
2806 } else {
2807 None
2808 },
2809 ..Default::default()
2810 })
2811 }
2812
2813 async fn register_table(
2814 &self,
2815 request: lance_namespace::models::RegisterTableRequest,
2816 ) -> Result<lance_namespace::models::RegisterTableResponse> {
2817 self.record_op("register_table");
2818 if let Some(ref manifest_ns) = self.manifest_ns {
2820 return LanceNamespace::register_table(manifest_ns.as_ref(), request).await;
2821 }
2822
2823 Err(NamespaceError::Unsupported {
2825 message: "register_table is only supported when manifest mode is enabled".to_string(),
2826 }
2827 .into())
2828 }
2829
2830 async fn deregister_table(
2831 &self,
2832 request: lance_namespace::models::DeregisterTableRequest,
2833 ) -> Result<lance_namespace::models::DeregisterTableResponse> {
2834 self.record_op("deregister_table");
2835 if let Some(ref manifest_ns) = self.manifest_ns {
2837 return LanceNamespace::deregister_table(manifest_ns.as_ref(), request).await;
2838 }
2839
2840 let table_name = Self::table_name_from_id(&request.id)?;
2842 let table_uri = self.table_full_uri(&table_name);
2843
2844 let status = self.check_table_status(&table_name).await;
2847
2848 if !status.exists {
2849 return Err(NamespaceError::TableNotFound {
2850 message: table_name.to_string(),
2851 }
2852 .into());
2853 }
2854
2855 if status.is_deregistered {
2856 return Err(NamespaceError::TableNotFound {
2857 message: format!("Table is already deregistered: {}", table_name),
2858 }
2859 .into());
2860 }
2861
2862 let deregistered_path = self.table_deregistered_file_path(&table_name);
2868 self.put_marker_file_atomic(
2869 &deregistered_path,
2870 &format!("deregistration marker for table {}", table_name),
2871 )
2872 .await
2873 .map_err(|e| {
2874 if e.contains("already exists") {
2875 lance_core::Error::from(NamespaceError::InvalidTableState {
2876 message: format!("Table is already deregistered: {}", table_name),
2877 })
2878 } else {
2879 lance_core::Error::from(NamespaceError::Internal { message: e })
2880 }
2881 })?;
2882
2883 Ok(lance_namespace::models::DeregisterTableResponse {
2884 id: request.id,
2885 location: Some(table_uri),
2886 ..Default::default()
2887 })
2888 }
2889
2890 async fn list_table_versions(
2891 &self,
2892 request: ListTableVersionsRequest,
2893 ) -> Result<ListTableVersionsResponse> {
2894 self.record_op("list_table_versions");
2895 let branch = Self::normalized_branch(request.branch.as_deref())?;
2896 let table_uri = self.resolve_table_location(&request.id).await?;
2897 let table_uri = match branch {
2898 Some(b) => self.resolve_branch_location(&table_uri, b).await?,
2899 None => table_uri,
2900 };
2901 let want_descending = request.descending == Some(true);
2902 let table_versions = self
2903 .list_table_versions_from_storage(&table_uri, want_descending, request.limit)
2904 .await?;
2905
2906 Ok(ListTableVersionsResponse {
2907 versions: table_versions,
2908 page_token: None,
2909 })
2910 }
2911
2912 async fn create_table_version(
2913 &self,
2914 request: CreateTableVersionRequest,
2915 ) -> Result<CreateTableVersionResponse> {
2916 self.record_op("create_table_version");
2917 let branch = Self::normalized_branch(request.branch.as_deref())?;
2918 let table_uri = self.resolve_table_location(&request.id).await?;
2919 let table_uri = match branch {
2920 Some(b) => self.resolve_branch_location(&table_uri, b).await?,
2921 None => table_uri,
2922 };
2923
2924 let staging_manifest_path = &request.manifest_path;
2925 let version = request.version as u64;
2926
2927 let table_path = self.object_store_path_from_uri(&table_uri)?;
2928
2929 let naming_scheme = match request.naming_scheme.as_deref() {
2931 Some("V1") => ManifestNamingScheme::V1,
2932 _ => ManifestNamingScheme::V2,
2933 };
2934
2935 let final_path = naming_scheme.manifest_path(&table_path, version);
2937
2938 let staging_path = Path::parse(staging_manifest_path).map_err(|e| {
2939 lance_core::Error::from(NamespaceError::InvalidInput {
2940 message: format!(
2941 "Invalid staging manifest path '{}': {}",
2942 staging_manifest_path, e
2943 ),
2944 })
2945 })?;
2946
2947 let copy_result = match self
2948 .object_store
2949 .inner
2950 .copy_if_not_exists(&staging_path, &final_path)
2951 .await
2952 {
2953 Ok(()) => Ok(()),
2954 Err(ObjectStoreError::NotImplemented { .. })
2955 | Err(ObjectStoreError::NotSupported { .. }) => {
2956 let manifest_data = self
2957 .object_store
2958 .inner
2959 .get(&staging_path)
2960 .await
2961 .map_err(|e| {
2962 lance_core::Error::from(NamespaceError::Internal {
2963 message: format!(
2964 "Failed to read staging manifest at '{}': {}",
2965 staging_manifest_path, e
2966 ),
2967 })
2968 })?
2969 .bytes()
2970 .await
2971 .map_err(|e| {
2972 lance_core::Error::from(NamespaceError::Internal {
2973 message: format!(
2974 "Failed to read staging manifest bytes at '{}': {}",
2975 staging_manifest_path, e
2976 ),
2977 })
2978 })?;
2979 self.object_store
2980 .inner
2981 .put_opts(
2982 &final_path,
2983 manifest_data.into(),
2984 PutOptions {
2985 mode: PutMode::Create,
2986 ..Default::default()
2987 },
2988 )
2989 .await
2990 .map(|_| ())
2991 }
2992 Err(e) => Err(e),
2993 };
2994
2995 match copy_result {
2996 Ok(()) => {}
2997 Err(ObjectStoreError::AlreadyExists { .. })
2998 | Err(ObjectStoreError::Precondition { .. }) => {
2999 return Err(lance_core::Error::from(
3000 NamespaceError::ConcurrentModification {
3001 message: format!(
3002 "Version {} already exists for table at '{}'",
3003 version, table_uri
3004 ),
3005 },
3006 ));
3007 }
3008 Err(e) => {
3009 return Err(lance_core::Error::from(NamespaceError::Internal {
3010 message: format!(
3011 "Failed to create version {} for table at '{}': {}",
3012 version, table_uri, e
3013 ),
3014 }));
3015 }
3016 }
3017
3018 let final_meta = self
3019 .object_store
3020 .inner
3021 .head(&final_path)
3022 .await
3023 .map_err(|e| {
3024 lance_core::Error::from(NamespaceError::Internal {
3025 message: format!(
3026 "Failed to stat created version {} for table at '{}': {}",
3027 version, table_uri, e
3028 ),
3029 })
3030 })?;
3031 let manifest_size = final_meta.size as i64;
3032
3033 if let Err(e) = self.object_store.inner.delete(&staging_path).await {
3035 log::warn!(
3036 "Failed to delete staging manifest at '{}': {:?}",
3037 staging_path,
3038 e
3039 );
3040 }
3041
3042 Ok(CreateTableVersionResponse {
3043 transaction_id: None,
3044 version: Some(Box::new(TableVersion {
3045 version: version as i64,
3046 manifest_path: final_path.to_string(),
3047 manifest_size: Some(manifest_size),
3048 e_tag: final_meta.e_tag,
3049 timestamp_millis: None,
3050 metadata: None,
3051 })),
3052 })
3053 }
3054
3055 async fn describe_table_version(
3056 &self,
3057 request: DescribeTableVersionRequest,
3058 ) -> Result<DescribeTableVersionResponse> {
3059 self.record_op("describe_table_version");
3060 let branch = Self::normalized_branch(request.branch.as_deref())?;
3061 let table_uri = self.resolve_table_location(&request.id).await?;
3062 let table_uri = match branch {
3063 Some(b) => self.resolve_branch_location(&table_uri, b).await?,
3064 None => table_uri,
3065 };
3066 let versions = self
3067 .list_table_versions_from_storage(&table_uri, true, None)
3068 .await?;
3069 let table_version = if let Some(requested_version) = request.version {
3070 versions
3071 .into_iter()
3072 .find(|version| version.version == requested_version)
3073 .ok_or_else(|| {
3074 lance_core::Error::from(NamespaceError::TableVersionNotFound {
3075 message: format!(
3076 "version {} for table {}",
3077 requested_version,
3078 Self::format_table_id_from_request(&request.id)
3079 ),
3080 })
3081 })?
3082 } else {
3083 versions.into_iter().next().ok_or_else(|| {
3084 lance_core::Error::from(NamespaceError::TableVersionNotFound {
3085 message: format!(
3086 "latest version for table {}",
3087 Self::format_table_id_from_request(&request.id)
3088 ),
3089 })
3090 })?
3091 };
3092
3093 Ok(DescribeTableVersionResponse {
3094 version: Box::new(table_version),
3095 })
3096 }
3097
3098 async fn batch_delete_table_versions(
3099 &self,
3100 request: BatchDeleteTableVersionsRequest,
3101 ) -> Result<BatchDeleteTableVersionsResponse> {
3102 self.record_op("batch_delete_table_versions");
3103 let branch = Self::normalized_branch(request.branch.as_deref())?;
3104 let ranges: Vec<(i64, i64)> = request
3107 .ranges
3108 .iter()
3109 .map(|r| (r.start_version, r.end_version))
3110 .collect();
3111
3112 const MAX_VERSIONS_PER_REQUEST: i128 = 1_000_000;
3116 let requested: i128 = ranges
3117 .iter()
3118 .map(|(s, e)| {
3119 if *e < 0 {
3120 0
3121 } else {
3122 (*e as i128 - *s as i128).max(0)
3123 }
3124 })
3125 .sum();
3126 if requested > MAX_VERSIONS_PER_REQUEST {
3127 return Err(NamespaceError::InvalidInput {
3128 message: format!(
3129 "batch_delete requested {} versions; limit is {}",
3130 requested, MAX_VERSIONS_PER_REQUEST
3131 ),
3132 }
3133 .into());
3134 }
3135
3136 let table_entries = vec![TableDeleteEntry {
3137 table_id: request.id.clone(),
3138 ranges,
3139 }];
3140
3141 let total_deleted_count = self
3142 .delete_physical_version_files(&table_entries, branch)
3143 .await?;
3144
3145 Ok(BatchDeleteTableVersionsResponse {
3146 deleted_count: Some(total_deleted_count),
3147 transaction_id: None,
3148 })
3149 }
3150
3151 async fn create_table_index(
3152 &self,
3153 request: CreateTableIndexRequest,
3154 ) -> Result<CreateTableIndexResponse> {
3155 self.record_op("create_table_index");
3156 let table_uri = self.resolve_table_location(&request.id).await?;
3157 let mut dataset = self
3158 .load_dataset(&table_uri, None, "create_table_index")
3159 .await?;
3160 let index_request = Self::build_index_params(&request)?;
3161
3162 dataset
3163 .create_index(
3164 &[request.column.as_str()],
3165 index_request.index_type(),
3166 request.name.clone(),
3167 index_request.params(),
3168 false,
3169 )
3170 .await
3171 .map_err(|e| {
3172 let err_msg = format!("{}", e);
3173 let ns_err = if err_msg.contains("already exists") {
3174 NamespaceError::TableIndexAlreadyExists {
3175 message: format!(
3176 "Index '{}' already exists on table '{}': {:?}",
3177 request.name.as_deref().unwrap_or("<auto-generated>"),
3178 table_uri,
3179 e
3180 ),
3181 }
3182 } else if err_msg.contains("not found") || err_msg.contains("does not exist") {
3183 NamespaceError::TableColumnNotFound {
3184 message: format!(
3185 "Column '{}' not found for table '{}': {:?}",
3186 request.column, table_uri, e
3187 ),
3188 }
3189 } else {
3190 NamespaceError::Internal {
3191 message: format!(
3192 "Failed to create {} index '{}' on column '{}' for table '{}': {:?}",
3193 request.index_type,
3194 request.name.as_deref().unwrap_or("<auto-generated>"),
3195 request.column,
3196 table_uri,
3197 e
3198 ),
3199 }
3200 };
3201 lance_core::Error::from(ns_err)
3202 })?;
3203
3204 let transaction_id = dataset
3205 .read_transaction()
3206 .await
3207 .map_err(|e| {
3208 lance_core::Error::from(NamespaceError::Internal {
3209 message: format!(
3210 "Failed to read committed transaction after creating index on '{}': {}",
3211 table_uri, e
3212 ),
3213 })
3214 })?
3215 .map(|transaction| transaction.uuid);
3216
3217 Ok(CreateTableIndexResponse { transaction_id })
3218 }
3219
3220 async fn list_table_indices(
3221 &self,
3222 request: ListTableIndicesRequest,
3223 ) -> Result<ListTableIndicesResponse> {
3224 self.record_op("list_table_indices");
3225 let table_uri = self.resolve_table_location(&request.id).await?;
3226 let dataset = self
3227 .load_dataset(&table_uri, request.version, "list_table_indices")
3228 .await?;
3229 let total_rows = dataset.count_rows(None).await.map_err(|e| {
3230 lance_core::Error::from(NamespaceError::Internal {
3231 message: format!("Failed to count rows for table '{}': {:?}", table_uri, e),
3232 })
3233 })? as u64;
3234 let mut indices = dataset
3235 .describe_indices(None)
3236 .await
3237 .map_err(|e| {
3238 lance_core::Error::from(NamespaceError::Internal {
3239 message: format!("Failed to describe table indices for '{}': {:?}", table_uri, e),
3240 })
3241 })?
3242 .into_iter()
3243 .filter(|description| {
3244 description
3245 .metadata()
3246 .first()
3247 .map(|metadata| !is_system_index(metadata))
3248 .unwrap_or(false)
3249 })
3250 .map(|description| {
3251 let columns = description
3252 .field_ids()
3253 .iter()
3254 .map(|field_id| {
3255 dataset
3256 .schema()
3257 .field_path(i32::try_from(*field_id).map_err(|e| {
3258 lance_core::Error::from(NamespaceError::Internal {
3259 message: format!(
3260 "Field id {} does not fit in i32 for table '{}': {}",
3261 field_id, table_uri, e
3262 ),
3263 })
3264 })?)
3265 .map_err(|e| {
3266 lance_core::Error::from(NamespaceError::Internal {
3267 message: format!(
3268 "Failed to resolve field path for field_id {} in table '{}': {}",
3269 field_id, table_uri, e
3270 ),
3271 })
3272 })
3273 })
3274 .collect::<Result<Vec<_>>>()?;
3275
3276 let segments = description.segments();
3277 let created_at = segments
3278 .iter()
3279 .filter_map(|segment| segment.created_at)
3280 .min()
3281 .map(|ts| ts.to_rfc3339());
3282
3283 #[allow(clippy::needless_update)]
3286 let content = IndexContent {
3287 index_name: description.name().to_string(),
3288 index_uuid: description.metadata()[0].uuid.to_string(),
3289 columns,
3290 status: "SUCCEEDED".to_string(),
3291 index_type: Some(description.index_type().to_string()),
3292 type_url: Some(description.type_url().to_string()),
3293 num_indexed_rows: Some(description.rows_indexed() as i64),
3294 num_unindexed_rows: Some(
3295 total_rows.saturating_sub(description.rows_indexed()) as i64,
3296 ),
3297 size_bytes: description.total_size_bytes().map(|size| size as i64),
3298 num_segments: Some(segments.len() as i32),
3299 created_at,
3300 index_version: segments.first().map(|segment| segment.index_version),
3301 index_details: description.details().ok(),
3302 ..Default::default()
3303 };
3304 Ok(content)
3305 })
3306 .collect::<Result<Vec<_>>>()?;
3307
3308 let page_token = Self::paginate_indices(&mut indices, request.page_token, request.limit);
3309 Ok(ListTableIndicesResponse {
3310 indexes: indices,
3311 page_token,
3312 })
3313 }
3314
3315 async fn describe_table_index_stats(
3316 &self,
3317 request: DescribeTableIndexStatsRequest,
3318 ) -> Result<DescribeTableIndexStatsResponse> {
3319 self.record_op("describe_table_index_stats");
3320 let table_uri = self.resolve_table_location(&request.id).await?;
3321 let dataset = self
3322 .load_dataset(&table_uri, request.version, "describe_table_index_stats")
3323 .await?;
3324 let index_name = request.index_name.as_deref().ok_or_else(|| {
3325 lance_core::Error::from(NamespaceError::InvalidInput {
3326 message: "Index name is required for describe_table_index_stats".to_string(),
3327 })
3328 })?;
3329 let metadatas = dataset
3330 .load_indices_by_name(index_name)
3331 .await
3332 .map_err(|e| {
3333 lance_core::Error::from(NamespaceError::TableIndexNotFound {
3334 message: format!(
3335 "Failed to load index '{}' metadata for table '{}': {}",
3336 index_name, table_uri, e
3337 ),
3338 })
3339 })?;
3340 if metadatas.first().is_some_and(is_system_index) {
3341 return Err(NamespaceError::Unsupported {
3342 message: format!("System index '{}' is not exposed by this API", index_name),
3343 }
3344 .into());
3345 }
3346
3347 let stats = <Dataset as DatasetIndexExt>::index_statistics(&dataset, index_name)
3348 .await
3349 .map_err(|e| {
3350 lance_core::Error::from(NamespaceError::TableIndexNotFound {
3351 message: format!(
3352 "Failed to describe index statistics for '{}' on table '{}': {}",
3353 index_name, table_uri, e
3354 ),
3355 })
3356 })?;
3357 let stats: serde_json::Value = serde_json::from_str(&stats).map_err(|e| {
3358 lance_core::Error::from(NamespaceError::Internal {
3359 message: format!(
3360 "Failed to parse index statistics for '{}' on table '{}': {}",
3361 index_name, table_uri, e
3362 ),
3363 })
3364 })?;
3365
3366 Ok(Self::describe_table_index_stats_response(&stats))
3367 }
3368
3369 async fn describe_transaction(
3370 &self,
3371 request: DescribeTransactionRequest,
3372 ) -> Result<DescribeTransactionResponse> {
3373 self.record_op("describe_transaction");
3374 let mut request_id = request.id.ok_or_else(|| {
3375 lance_core::Error::from(NamespaceError::InvalidInput {
3376 message: "Transaction id must include table id and transaction identifier"
3377 .to_string(),
3378 })
3379 })?;
3380 if request_id.len() < 2 {
3381 return Err(NamespaceError::InvalidInput {
3382 message: format!(
3383 "Transaction request id must include table id and transaction identifier, got {:?}",
3384 request_id
3385 ),
3386 }
3387 .into());
3388 }
3389
3390 let id = request_id.pop().expect("request_id len checked above");
3391 let table_id = Some(request_id);
3392 let table_uri = self.resolve_table_location(&table_id).await?;
3393 let dataset = self
3394 .load_dataset(&table_uri, None, "describe_transaction")
3395 .await?;
3396 let (version, transaction) = self.find_transaction(&dataset, &id).await?;
3397
3398 Ok(Self::transaction_response(version, &transaction))
3399 }
3400
3401 async fn create_table_scalar_index(
3402 &self,
3403 request: CreateTableIndexRequest,
3404 ) -> Result<CreateTableScalarIndexResponse> {
3405 self.record_op("create_table_scalar_index");
3406 let index_type = Self::parse_index_type(&request.index_type)?;
3407 if !index_type.is_scalar() {
3408 return Err(NamespaceError::InvalidInput {
3409 message: format!(
3410 "create_table_scalar_index only supports scalar index types, got {}",
3411 request.index_type
3412 ),
3413 }
3414 .into());
3415 }
3416
3417 let response = self.create_table_index(request).await?;
3418 Ok(CreateTableScalarIndexResponse {
3419 transaction_id: response.transaction_id,
3420 })
3421 }
3422
3423 async fn drop_table_index(
3424 &self,
3425 request: DropTableIndexRequest,
3426 ) -> Result<DropTableIndexResponse> {
3427 self.record_op("drop_table_index");
3428 let table_uri = self.resolve_table_location(&request.id).await?;
3429 let index_name = request.index_name.as_deref().ok_or_else(|| {
3430 lance_core::Error::from(NamespaceError::InvalidInput {
3431 message: "Index name is required for drop_table_index".to_string(),
3432 })
3433 })?;
3434 let mut dataset = self
3435 .load_dataset(&table_uri, None, "drop_table_index")
3436 .await?;
3437 let metadatas = dataset
3438 .load_indices_by_name(index_name)
3439 .await
3440 .map_err(|e| {
3441 lance_core::Error::from(NamespaceError::TableIndexNotFound {
3442 message: format!(
3443 "Failed to load index '{}' before dropping it from table '{}': {}",
3444 index_name, table_uri, e
3445 ),
3446 })
3447 })?;
3448 if metadatas.first().is_some_and(is_system_index) {
3449 return Err(NamespaceError::Unsupported {
3450 message: format!(
3451 "System index '{}' cannot be dropped via this API",
3452 index_name
3453 ),
3454 }
3455 .into());
3456 }
3457
3458 dataset.drop_index(index_name).await.map_err(|e| {
3459 lance_core::Error::from(NamespaceError::TableIndexNotFound {
3460 message: format!(
3461 "Failed to drop index '{}' from table '{}': {}",
3462 index_name, table_uri, e
3463 ),
3464 })
3465 })?;
3466
3467 let transaction_id = dataset
3468 .read_transaction()
3469 .await
3470 .map_err(|e| {
3471 lance_core::Error::from(NamespaceError::Internal {
3472 message: format!(
3473 "Failed to read committed transaction after dropping index '{}' from '{}': {}",
3474 index_name, table_uri, e
3475 ),
3476 })
3477 })?
3478 .map(|transaction| transaction.uuid);
3479
3480 Ok(DropTableIndexResponse { transaction_id })
3481 }
3482
3483 async fn list_all_tables(&self, request: ListTablesRequest) -> Result<ListTablesResponse> {
3484 let mut tables = self.list_directory_tables().await?;
3487 tables = self
3488 .filter_declared_tables(tables, request.include_declared.unwrap_or(true))
3489 .await?;
3490 Self::apply_pagination(&mut tables, request.page_token, request.limit);
3491 Ok(ListTablesResponse::new(tables))
3492 }
3493
3494 async fn restore_table(&self, request: RestoreTableRequest) -> Result<RestoreTableResponse> {
3495 let version = request.version;
3496 if version < 0 {
3497 return Err(Error::invalid_input_source(
3498 format!(
3499 "Table version for restore_table must be non-negative, got {}",
3500 version
3501 )
3502 .into(),
3503 ));
3504 }
3505
3506 let branch = Self::normalized_branch(request.branch.as_deref())?;
3507 let table_uri = self.resolve_table_location(&request.id).await?;
3508 let mut dataset = match branch {
3509 Some(branch) => self.open_validated_branch(&table_uri, branch).await?,
3510 None => self.load_dataset(&table_uri, None, "restore_table").await?,
3511 };
3512
3513 dataset = dataset
3514 .checkout_version(version as u64)
3515 .await
3516 .map_err(|e| {
3517 Error::namespace_source(
3518 format!(
3519 "Failed to checkout version {} for restore at '{}': {}",
3520 version, table_uri, e
3521 )
3522 .into(),
3523 )
3524 })?;
3525
3526 dataset.restore().await.map_err(|e| {
3527 Error::namespace_source(
3528 format!(
3529 "Failed to restore table at '{}' to version {}: {}",
3530 table_uri, version, e
3531 )
3532 .into(),
3533 )
3534 })?;
3535
3536 let transaction_id = dataset
3537 .read_transaction()
3538 .await
3539 .map_err(|e| {
3540 Error::namespace_source(
3541 format!(
3542 "Failed to read transaction after restoring '{}': {}",
3543 table_uri, e
3544 )
3545 .into(),
3546 )
3547 })?
3548 .map(|t| t.uuid);
3549
3550 Ok(RestoreTableResponse { transaction_id })
3551 }
3552
3553 async fn update_table_schema_metadata(
3554 &self,
3555 request: UpdateTableSchemaMetadataRequest,
3556 ) -> Result<UpdateTableSchemaMetadataResponse> {
3557 let table_uri = self.resolve_table_location(&request.id).await?;
3558 let mut dataset = self
3559 .load_dataset(&table_uri, None, "update_table_schema_metadata")
3560 .await?;
3561
3562 let new_metadata = request.metadata.unwrap_or_default();
3563 let updated_metadata = dataset
3564 .update_schema_metadata(new_metadata.iter().map(|(k, v)| (k.as_str(), v.as_str())))
3565 .await
3566 .map_err(|e| {
3567 Error::namespace_source(
3568 format!(
3569 "Failed to update schema metadata for table at '{}': {}",
3570 table_uri, e
3571 )
3572 .into(),
3573 )
3574 })?;
3575
3576 let transaction_id = dataset
3577 .read_transaction()
3578 .await
3579 .map_err(|e| {
3580 Error::namespace_source(
3581 format!(
3582 "Failed to read transaction after updating metadata for '{}': {}",
3583 table_uri, e
3584 )
3585 .into(),
3586 )
3587 })?
3588 .map(|t| t.uuid);
3589
3590 Ok(UpdateTableSchemaMetadataResponse {
3591 metadata: Some(updated_metadata),
3592 transaction_id,
3593 })
3594 }
3595
3596 async fn get_table_stats(
3597 &self,
3598 request: GetTableStatsRequest,
3599 ) -> Result<GetTableStatsResponse> {
3600 let table_uri = self.resolve_table_location(&request.id).await?;
3601 let dataset = Arc::new(
3602 self.load_dataset(&table_uri, None, "get_table_stats")
3603 .await?,
3604 );
3605
3606 let data_stats = dataset.calculate_data_stats().await.map_err(|e| {
3608 Error::namespace_source(
3609 format!(
3610 "Failed to calculate data statistics for table at '{}': {}",
3611 table_uri, e
3612 )
3613 .into(),
3614 )
3615 })?;
3616 let total_bytes: i64 = data_stats
3617 .fields
3618 .iter()
3619 .map(|f| f.bytes_on_disk as i64)
3620 .sum();
3621
3622 let fragment_row_futures: Vec<_> = dataset
3624 .get_fragments()
3625 .into_iter()
3626 .map(|f| async move { f.physical_rows().await })
3627 .collect();
3628 let fragment_row_results = futures::future::join_all(fragment_row_futures).await;
3629 let mut fragment_row_counts: Vec<i64> = fragment_row_results
3630 .into_iter()
3631 .filter_map(|r| r.ok())
3632 .map(|r| r as i64)
3633 .collect();
3634
3635 let num_fragments = fragment_row_counts.len() as i64;
3636 let num_rows: i64 = fragment_row_counts.iter().sum();
3637
3638 const SMALL_FRAGMENT_THRESHOLD: i64 = 1024 * 1024;
3641 let num_small_fragments = fragment_row_counts
3642 .iter()
3643 .filter(|&&r| r < SMALL_FRAGMENT_THRESHOLD)
3644 .count() as i64;
3645
3646 fragment_row_counts.sort_unstable();
3648 let lengths = if fragment_row_counts.is_empty() {
3649 FragmentSummary::new(0, 0, 0, 0, 0, 0, 0)
3650 } else {
3651 let len = fragment_row_counts.len();
3652 let min = fragment_row_counts[0];
3653 let max = fragment_row_counts[len - 1];
3654 let mean = num_rows / num_fragments;
3655 let pct = |p: f64| fragment_row_counts[((len - 1) as f64 * p) as usize];
3656 FragmentSummary::new(min, max, mean, pct(0.25), pct(0.50), pct(0.75), pct(0.99))
3657 };
3658
3659 let indices = dataset.load_indices().await.map_err(|e| {
3661 Error::namespace_source(
3662 format!("Failed to load indices for table at '{}': {}", table_uri, e).into(),
3663 )
3664 })?;
3665 let num_indices = indices.iter().filter(|m| !is_system_index(m)).count() as i64;
3666
3667 let fragment_stats = FragmentStats::new(num_fragments, num_small_fragments, lengths);
3668 Ok(GetTableStatsResponse::new(
3669 total_bytes,
3670 num_rows,
3671 num_indices,
3672 fragment_stats,
3673 ))
3674 }
3675
3676 async fn explain_table_query_plan(
3677 &self,
3678 request: ExplainTableQueryPlanRequest,
3679 ) -> Result<String> {
3680 let table_uri = self.resolve_table_location(&request.id).await?;
3681 let dataset = self
3682 .load_dataset(
3683 &table_uri,
3684 request.query.version,
3685 "explain_table_query_plan",
3686 )
3687 .await?;
3688 let verbose = request.verbose.unwrap_or(false);
3689
3690 let mut scanner = dataset.scan();
3691 Self::apply_query_params_to_scanner(
3692 &mut scanner,
3693 request.query.filter.as_deref(),
3694 request.query.columns.as_deref(),
3695 request.query.vector_column.as_deref(),
3696 &request.query.vector,
3697 request.query.k,
3698 request.query.offset,
3699 request.query.prefilter,
3700 request.query.bypass_vector_index,
3701 request.query.nprobes,
3702 request.query.ef,
3703 request.query.refine_factor,
3704 request.query.distance_type.as_deref(),
3705 request.query.fast_search,
3706 request.query.with_row_id,
3707 request.query.lower_bound,
3708 request.query.upper_bound,
3709 "explain_table_query_plan",
3710 )?;
3711
3712 scanner.explain_plan(verbose).await.map_err(|e| {
3713 Error::namespace_source(
3714 format!(
3715 "Failed to explain query plan for table at '{}': {}",
3716 table_uri, e
3717 )
3718 .into(),
3719 )
3720 })
3721 }
3722
3723 async fn analyze_table_query_plan(
3724 &self,
3725 request: AnalyzeTableQueryPlanRequest,
3726 ) -> Result<String> {
3727 let table_uri = self.resolve_table_location(&request.id).await?;
3728 let dataset = self
3729 .load_dataset(&table_uri, request.version, "analyze_table_query_plan")
3730 .await?;
3731
3732 let mut scanner = dataset.scan();
3733 Self::apply_query_params_to_scanner(
3734 &mut scanner,
3735 request.filter.as_deref(),
3736 request.columns.as_deref(),
3737 request.vector_column.as_deref(),
3738 &request.vector,
3739 request.k,
3740 request.offset,
3741 request.prefilter,
3742 request.bypass_vector_index,
3743 request.nprobes,
3744 request.ef,
3745 request.refine_factor,
3746 request.distance_type.as_deref(),
3747 request.fast_search,
3748 request.with_row_id,
3749 request.lower_bound,
3750 request.upper_bound,
3751 "analyze_table_query_plan",
3752 )?;
3753
3754 scanner.analyze_plan().await.map_err(|e| {
3755 Error::namespace_source(
3756 format!(
3757 "Failed to analyze query plan for table at '{}': {}",
3758 table_uri, e
3759 )
3760 .into(),
3761 )
3762 })
3763 }
3764
3765 async fn count_table_rows(&self, request: CountTableRowsRequest) -> Result<i64> {
3766 self.record_op("count_table_rows");
3767 let table_uri = self.resolve_table_location(&request.id).await?;
3768 let dataset = self
3769 .load_dataset(&table_uri, request.version, "count_table_rows")
3770 .await?;
3771
3772 let count =
3773 dataset
3774 .count_rows(request.predicate)
3775 .await
3776 .map_err(|e| NamespaceError::Internal {
3777 message: format!("Failed to count rows for table at '{}': {:?}", table_uri, e),
3778 })?;
3779
3780 Ok(count as i64)
3781 }
3782
3783 async fn insert_into_table(
3784 &self,
3785 request: InsertIntoTableRequest,
3786 request_data: Bytes,
3787 ) -> Result<InsertIntoTableResponse> {
3788 self.record_op("insert_into_table");
3789 let table_uri = self.resolve_table_location(&request.id).await?;
3790 let (reader, _num_rows) =
3791 Self::ipc_reader_from_request_data(&request_data, "insert_into_table")?;
3792
3793 let mode = match request.mode.as_deref() {
3794 Some(m) if m.eq_ignore_ascii_case("overwrite") => WriteMode::Overwrite,
3795 Some(m) if m.eq_ignore_ascii_case("append") => WriteMode::Append,
3796 None => WriteMode::Append,
3797 Some(m) => {
3798 return Err(lance_namespace::error::NamespaceError::InvalidInput {
3799 message: format!(
3800 "Unsupported write mode '{}'. Supported modes are: 'append', 'overwrite'",
3801 m
3802 ),
3803 }
3804 .into());
3805 }
3806 };
3807
3808 if !self.table_uri_has_actual_manifests(&table_uri).await? {
3809 self.write_reader_to_table(&table_uri, reader, WriteMode::Create, None)
3810 .await?;
3811 } else {
3812 self.write_reader_to_table(&table_uri, reader, mode, None)
3813 .await?;
3814 }
3815
3816 Ok(InsertIntoTableResponse {
3817 transaction_id: None,
3818 })
3819 }
3820
3821 async fn merge_insert_into_table(
3822 &self,
3823 request: MergeInsertIntoTableRequest,
3824 request_data: Bytes,
3825 ) -> Result<MergeInsertIntoTableResponse> {
3826 self.record_op("merge_insert_into_table");
3827 let table_uri = self.resolve_table_location(&request.id).await?;
3828 let on = request.on.as_ref().ok_or_else(|| {
3829 lance_core::Error::from(NamespaceError::InvalidInput {
3830 message: "'on' field is required for merge_insert_into_table".to_string(),
3831 })
3832 })?;
3833
3834 let table_has_manifests = self.table_uri_has_actual_manifests(&table_uri).await?;
3835 let (reader, num_rows) =
3836 Self::ipc_reader_from_request_data(&request_data, "merge_insert_into_table")?;
3837
3838 if !table_has_manifests {
3839 let dataset = self
3840 .write_reader_to_table(&table_uri, reader, WriteMode::Create, None)
3841 .await?;
3842 let version = dataset.version().version as i64;
3843 return Ok(MergeInsertIntoTableResponse {
3844 transaction_id: None,
3845 num_updated_rows: Some(0),
3846 num_inserted_rows: Some(num_rows as i64),
3847 num_deleted_rows: Some(0),
3848 version: Some(version),
3849 });
3850 }
3851
3852 let dataset = Arc::new(
3853 self.load_dataset(&table_uri, None, "merge_insert_into_table")
3854 .await?,
3855 );
3856
3857 let mut merge_builder = MergeInsertBuilder::try_new(dataset.clone(), vec![on.clone()])
3858 .map_err(|e| {
3859 lance_core::Error::from(NamespaceError::InvalidInput {
3860 message: format!("Failed to create merge_insert_into_table builder: {}", e),
3861 })
3862 })?;
3863
3864 if let Some(filter) = request.when_matched_update_all_filt.as_deref() {
3865 let behavior = WhenMatched::update_if(dataset.as_ref(), filter).map_err(|e| {
3866 lance_core::Error::from(NamespaceError::InvalidInput {
3867 message: format!(
3868 "Invalid when_matched_update_all_filt for merge_insert_into_table: {}",
3869 e
3870 ),
3871 })
3872 })?;
3873 merge_builder.when_matched(behavior);
3874 } else if request.when_matched_update_all.unwrap_or(false) {
3875 merge_builder.when_matched(WhenMatched::UpdateAll);
3876 }
3877
3878 if matches!(request.when_not_matched_insert_all, Some(false)) {
3879 merge_builder.when_not_matched(WhenNotMatched::DoNothing);
3880 } else {
3881 merge_builder.when_not_matched(WhenNotMatched::InsertAll);
3882 }
3883
3884 if let Some(filter) = request.when_not_matched_by_source_delete_filt.as_deref() {
3885 let behavior = WhenNotMatchedBySource::delete_if(dataset.as_ref(), filter).map_err(|e| {
3886 lance_core::Error::from(NamespaceError::InvalidInput {
3887 message: format!(
3888 "Invalid when_not_matched_by_source_delete_filt for merge_insert_into_table: {}",
3889 e
3890 ),
3891 })
3892 })?;
3893 merge_builder.when_not_matched_by_source(behavior);
3894 } else if request.when_not_matched_by_source_delete.unwrap_or(false) {
3895 merge_builder.when_not_matched_by_source(WhenNotMatchedBySource::Delete);
3896 }
3897
3898 if let Some(use_index) = request.use_index {
3899 merge_builder.use_index(use_index);
3900 }
3901
3902 let (dataset, stats) = merge_builder
3903 .try_build()
3904 .map_err(|e| {
3905 lance_core::Error::from(NamespaceError::InvalidInput {
3906 message: format!("Failed to build merge_insert_into_table job: {}", e),
3907 })
3908 })?
3909 .execute_reader(reader)
3910 .await
3911 .map_err(|e| NamespaceError::Internal {
3912 message: format!(
3913 "Failed to merge_insert_into_table at '{}': {}",
3914 table_uri, e
3915 ),
3916 })?;
3917
3918 Ok(MergeInsertIntoTableResponse {
3919 transaction_id: None,
3920 num_updated_rows: Some(stats.num_updated_rows as i64),
3921 num_inserted_rows: Some(stats.num_inserted_rows as i64),
3922 num_deleted_rows: Some(stats.num_deleted_rows as i64),
3923 version: Some(dataset.version().version as i64),
3924 })
3925 }
3926
3927 async fn query_table(&self, request: QueryTableRequest) -> Result<Bytes> {
3928 use arrow::ipc::writer::FileWriter;
3929
3930 self.record_op("query_table");
3931 let table_uri = self.resolve_table_location(&request.id).await?;
3932 let dataset = self
3933 .load_dataset(&table_uri, request.version, "query_table")
3934 .await?;
3935
3936 let mut scanner = dataset.scan();
3938
3939 let has_vector_query = request
3942 .vector
3943 .single_vector
3944 .as_ref()
3945 .map(|sv| !sv.is_empty())
3946 .unwrap_or(false)
3947 || request
3948 .vector
3949 .multi_vector
3950 .as_ref()
3951 .map(|mv| !mv.is_empty())
3952 .unwrap_or(false);
3953
3954 if let Some(prefilter) = request.prefilter {
3956 scanner.prefilter(prefilter);
3957 }
3958
3959 if has_vector_query {
3961 let vector_column = request.vector_column.as_deref().unwrap_or("vector");
3962
3963 let query_vector: Vec<f32> = request
3965 .vector
3966 .single_vector
3967 .clone()
3968 .or_else(|| {
3969 request
3970 .vector
3971 .multi_vector
3972 .as_ref()
3973 .and_then(|mv| mv.first().cloned())
3974 })
3975 .unwrap_or_default();
3976
3977 if !query_vector.is_empty() {
3978 let k = if request.k > 0 {
3979 request.k as usize
3980 } else {
3981 10
3982 };
3983 let query_array = Float32Array::from(query_vector);
3984 scanner
3985 .nearest(vector_column, &query_array, k)
3986 .map_err(|e| NamespaceError::InvalidInput {
3987 message: format!("Invalid vector search: {:?}", e),
3988 })?;
3989
3990 if let Some(ref distance_type) = request.distance_type {
3992 let metric = match distance_type.to_lowercase().as_str() {
3993 "l2" | "euclidean" => MetricType::L2,
3994 "cosine" => MetricType::Cosine,
3995 "dot" | "inner_product" => MetricType::Dot,
3996 "hamming" => MetricType::Hamming,
3997 _ => {
3998 return Err(NamespaceError::InvalidInput {
3999 message: format!("Unknown distance type: {}", distance_type),
4000 }
4001 .into());
4002 }
4003 };
4004 scanner.distance_metric(metric);
4005 }
4006
4007 if let Some(nprobes) = request.nprobes {
4009 scanner.minimum_nprobes(nprobes as usize);
4010 }
4011
4012 if let Some(ef) = request.ef {
4014 scanner.ef(ef as usize);
4015 }
4016
4017 if let Some(refine_factor) = request.refine_factor {
4019 scanner.refine(refine_factor as u32);
4020 }
4021
4022 if request.lower_bound.is_some() || request.upper_bound.is_some() {
4024 scanner.distance_range(request.lower_bound, request.upper_bound);
4025 }
4026
4027 if let Some(bypass) = request.bypass_vector_index {
4029 scanner.use_index(!bypass);
4030 }
4031
4032 if request.fast_search == Some(true) {
4034 scanner.fast_search();
4035 }
4036 }
4037 }
4038
4039 if let Some(ref fts_query) = request.full_text_query {
4041 if let Some(ref string_query) = fts_query.string_query {
4043 let mut fts = FullTextSearchQuery::new(string_query.query.clone());
4044
4045 if let Some(ref columns) = string_query.columns
4047 && !columns.is_empty()
4048 {
4049 fts = fts
4050 .with_columns(columns)
4051 .map_err(|e| NamespaceError::InvalidInput {
4052 message: format!("Invalid FTS columns: {:?}", e),
4053 })?;
4054 }
4055
4056 scanner
4057 .full_text_search(fts)
4058 .map_err(|e| NamespaceError::InvalidInput {
4059 message: format!("Invalid full text search: {:?}", e),
4060 })?;
4061 }
4062 }
4065
4066 if let Some(ref columns) = request.columns {
4068 if let Some(ref column_names) = columns.column_names
4069 && !column_names.is_empty()
4070 {
4071 scanner
4072 .project(column_names)
4073 .map_err(|e| NamespaceError::InvalidInput {
4074 message: format!("Invalid column projection: {:?}", e),
4075 })?;
4076 } else if let Some(ref column_aliases) = columns.column_aliases
4077 && !column_aliases.is_empty()
4078 {
4079 let transform_pairs: Vec<(String, String)> = column_aliases
4081 .iter()
4082 .map(|(alias, sql)| (alias.clone(), sql.clone()))
4083 .collect();
4084 scanner
4085 .project_with_transform(
4086 &transform_pairs
4087 .iter()
4088 .map(|(a, s)| (a.as_str(), s.as_str()))
4089 .collect::<Vec<_>>(),
4090 )
4091 .map_err(|e| NamespaceError::InvalidInput {
4092 message: format!("Invalid column alias expression: {:?}", e),
4093 })?;
4094 }
4095 }
4096
4097 if let Some(ref filter) = request.filter
4099 && !filter.is_empty()
4100 {
4101 scanner
4102 .filter(filter)
4103 .map_err(|e| NamespaceError::InvalidInput {
4104 message: format!("Invalid filter expression: {:?}", e),
4105 })?;
4106 }
4107
4108 if request.with_row_id == Some(true) {
4110 scanner.with_row_id();
4111 }
4112
4113 if !has_vector_query && request.k > 0 {
4117 let offset = request.offset.map(|o| o as i64);
4118 scanner.limit(Some(request.k as i64), offset).map_err(|e| {
4119 NamespaceError::InvalidInput {
4120 message: format!("Invalid limit/offset: {:?}", e),
4121 }
4122 })?;
4123 } else if has_vector_query && request.offset.is_some() {
4124 let offset = request.offset.map(|o| o as i64);
4126 scanner
4127 .limit(None, offset)
4128 .map_err(|e| NamespaceError::InvalidInput {
4129 message: format!("Invalid offset: {:?}", e),
4130 })?;
4131 }
4132
4133 let batch = scanner
4135 .try_into_batch()
4136 .await
4137 .map_err(|e| NamespaceError::Internal {
4138 message: format!("Failed to execute query: {:?}", e),
4139 })?;
4140
4141 let schema = batch.schema();
4143 let mut buffer = Vec::new();
4144 {
4145 let mut writer = FileWriter::try_new(&mut buffer, &schema).map_err(|e| {
4146 NamespaceError::Internal {
4147 message: format!("Failed to create IPC writer: {:?}", e),
4148 }
4149 })?;
4150 writer.write(&batch).map_err(|e| NamespaceError::Internal {
4151 message: format!("Failed to write batch to IPC: {:?}", e),
4152 })?;
4153 writer.finish().map_err(|e| NamespaceError::Internal {
4154 message: format!("Failed to finish IPC writer: {:?}", e),
4155 })?;
4156 }
4157
4158 Ok(Bytes::from(buffer))
4159 }
4160
4161 async fn list_table_tags(
4162 &self,
4163 request: ListTableTagsRequest,
4164 ) -> Result<ListTableTagsResponse> {
4165 self.record_op("list_table_tags");
4166 let table_uri = self.resolve_table_location(&request.id).await?;
4167 let dataset = self
4168 .load_dataset(&table_uri, None, "list_table_tags")
4169 .await?;
4170
4171 let raw_tags = dataset.tags().list().await.map_err(|e| {
4172 lance_core::Error::from(NamespaceError::Internal {
4173 message: format!("Failed to list tags for table at '{}': {}", table_uri, e),
4174 })
4175 })?;
4176
4177 let tags = raw_tags
4178 .into_iter()
4179 .map(|(name, contents)| {
4180 let mut tag_model =
4181 ModelTagContents::new(contents.version as i64, contents.manifest_size as i64);
4182 tag_model.branch = contents.branch;
4183 (name, tag_model)
4184 })
4185 .collect();
4186
4187 Ok(ListTableTagsResponse {
4188 tags,
4189 page_token: None,
4190 })
4191 }
4192
4193 async fn get_table_tag_version(
4194 &self,
4195 request: GetTableTagVersionRequest,
4196 ) -> Result<GetTableTagVersionResponse> {
4197 self.record_op("get_table_tag_version");
4198 if request.tag.is_empty() {
4199 return Err(NamespaceError::InvalidInput {
4200 message: "tag name must not be empty for get_table_tag_version".to_string(),
4201 }
4202 .into());
4203 }
4204
4205 let table_uri = self.resolve_table_location(&request.id).await?;
4206 let dataset = self
4207 .load_dataset(&table_uri, None, "get_table_tag_version")
4208 .await?;
4209
4210 let contents = dataset
4211 .tags()
4212 .get(&request.tag)
4213 .await
4214 .map_err(|e| Self::map_tag_error(e, &request.tag, &table_uri))?;
4215
4216 Ok(GetTableTagVersionResponse {
4217 version: contents.version as i64,
4218 branch: contents.branch,
4219 })
4220 }
4221
4222 async fn create_table_tag(
4223 &self,
4224 request: CreateTableTagRequest,
4225 ) -> Result<CreateTableTagResponse> {
4226 self.record_op("create_table_tag");
4227 if request.tag.is_empty() {
4228 return Err(NamespaceError::InvalidInput {
4229 message: "tag name must not be empty for create_table_tag".to_string(),
4230 }
4231 .into());
4232 }
4233 if request.version <= 0 {
4234 return Err(NamespaceError::InvalidInput {
4235 message: format!(
4236 "tag version must be a positive integer, got {} for create_table_tag",
4237 request.version
4238 ),
4239 }
4240 .into());
4241 }
4242
4243 let table_uri = self.resolve_table_location(&request.id).await?;
4244 let dataset = self
4245 .load_dataset(&table_uri, None, "create_table_tag")
4246 .await?;
4247
4248 dataset
4249 .tags()
4250 .create(&request.tag, request.version as u64)
4251 .await
4252 .map_err(|e| Self::map_tag_error(e, &request.tag, &table_uri))?;
4253
4254 Ok(CreateTableTagResponse {
4255 transaction_id: None,
4256 })
4257 }
4258
4259 async fn delete_table_tag(
4260 &self,
4261 request: DeleteTableTagRequest,
4262 ) -> Result<DeleteTableTagResponse> {
4263 self.record_op("delete_table_tag");
4264 if request.tag.is_empty() {
4265 return Err(NamespaceError::InvalidInput {
4266 message: "tag name must not be empty for delete_table_tag".to_string(),
4267 }
4268 .into());
4269 }
4270
4271 let table_uri = self.resolve_table_location(&request.id).await?;
4272 let dataset = self
4273 .load_dataset(&table_uri, None, "delete_table_tag")
4274 .await?;
4275
4276 dataset
4277 .tags()
4278 .delete(&request.tag)
4279 .await
4280 .map_err(|e| Self::map_tag_error(e, &request.tag, &table_uri))?;
4281
4282 Ok(DeleteTableTagResponse {
4283 transaction_id: None,
4284 })
4285 }
4286
4287 async fn update_table_tag(
4288 &self,
4289 request: UpdateTableTagRequest,
4290 ) -> Result<UpdateTableTagResponse> {
4291 self.record_op("update_table_tag");
4292 if request.tag.is_empty() {
4293 return Err(NamespaceError::InvalidInput {
4294 message: "tag name must not be empty for update_table_tag".to_string(),
4295 }
4296 .into());
4297 }
4298 if request.version <= 0 {
4299 return Err(NamespaceError::InvalidInput {
4300 message: format!(
4301 "tag version must be a positive integer, got {} for update_table_tag",
4302 request.version
4303 ),
4304 }
4305 .into());
4306 }
4307
4308 let table_uri = self.resolve_table_location(&request.id).await?;
4309 let dataset = self
4310 .load_dataset(&table_uri, None, "update_table_tag")
4311 .await?;
4312
4313 dataset
4314 .tags()
4315 .update(&request.tag, request.version as u64)
4316 .await
4317 .map_err(|e| Self::map_tag_error(e, &request.tag, &table_uri))?;
4318
4319 Ok(UpdateTableTagResponse {
4320 transaction_id: None,
4321 })
4322 }
4323
4324 async fn create_table_branch(
4325 &self,
4326 request: CreateTableBranchRequest,
4327 ) -> Result<CreateTableBranchResponse> {
4328 self.record_op("create_table_branch");
4329 if request.name.is_empty() {
4330 return Err(NamespaceError::InvalidInput {
4331 message: "branch name must not be empty for create_table_branch".to_string(),
4332 }
4333 .into());
4334 }
4335 let from_version = match request.from_version {
4336 Some(v) if v <= 0 => {
4337 return Err(NamespaceError::InvalidInput {
4338 message: format!(
4339 "from_version must be a positive integer, got {} for create_table_branch",
4340 v
4341 ),
4342 }
4343 .into());
4344 }
4345 Some(v) => Some(v as u64),
4346 None => None,
4347 };
4348
4349 let table_uri = self.resolve_table_location(&request.id).await?;
4350 let mut dataset = self
4351 .load_dataset(&table_uri, None, "create_table_branch")
4352 .await?;
4353
4354 if dataset.branches().get(&request.name).await.is_ok() {
4359 return Err(NamespaceError::TableBranchAlreadyExists {
4360 message: format!("branch '{}' for table at '{}'", request.name, table_uri),
4361 }
4362 .into());
4363 }
4364
4365 dataset
4366 .create_branch(
4367 &request.name,
4368 (request.from_branch.as_deref(), from_version),
4369 None,
4370 )
4371 .await
4372 .map_err(|e| {
4373 if matches!(e, lance_core::Error::DatasetNotFound { .. }) {
4376 NamespaceError::InvalidInput {
4377 message: format!(
4378 "from_branch/from_version for branch '{}' refers to a source that does not exist: {}",
4379 request.name, e
4380 ),
4381 }
4382 .into()
4383 } else {
4384 Self::map_branch_error(e, &request.name, &table_uri)
4385 }
4386 })?;
4387
4388 Ok(CreateTableBranchResponse {
4389 transaction_id: None,
4390 })
4391 }
4392
4393 async fn list_table_branches(
4394 &self,
4395 request: ListTableBranchesRequest,
4396 ) -> Result<ListTableBranchesResponse> {
4397 self.record_op("list_table_branches");
4398 let table_uri = self.resolve_table_location(&request.id).await?;
4399 let dataset = self
4400 .load_dataset(&table_uri, None, "list_table_branches")
4401 .await?;
4402
4403 let raw_branches = dataset.list_branches().await.map_err(|e| {
4404 lance_core::Error::from(NamespaceError::Internal {
4405 message: format!(
4406 "Failed to list branches for table at '{}': {}",
4407 table_uri, e
4408 ),
4409 })
4410 })?;
4411
4412 let branches = raw_branches
4413 .into_iter()
4414 .map(|(name, contents)| {
4415 let mut branch_model = ModelBranchContents::new(
4418 contents.parent_version as i64,
4419 contents.create_at as i64,
4420 contents.manifest_size as i64,
4421 );
4422 branch_model.parent_branch = contents.parent_branch;
4423 branch_model.metadata = if contents.metadata.is_empty() {
4424 None
4425 } else {
4426 Some(contents.metadata)
4427 };
4428 (name, branch_model)
4429 })
4430 .collect();
4431
4432 Ok(ListTableBranchesResponse {
4433 branches,
4434 page_token: None,
4435 })
4436 }
4437
4438 async fn delete_table_branch(
4439 &self,
4440 request: DeleteTableBranchRequest,
4441 ) -> Result<DeleteTableBranchResponse> {
4442 self.record_op("delete_table_branch");
4443 if request.name.is_empty() {
4444 return Err(NamespaceError::InvalidInput {
4445 message: "branch name must not be empty for delete_table_branch".to_string(),
4446 }
4447 .into());
4448 }
4449
4450 let table_uri = self.resolve_table_location(&request.id).await?;
4451 let mut dataset = self
4452 .load_dataset(&table_uri, None, "delete_table_branch")
4453 .await?;
4454
4455 dataset
4456 .delete_branch(&request.name)
4457 .await
4458 .map_err(|e| match e {
4459 lance_core::Error::RefConflict { message } => NamespaceError::InvalidInput {
4460 message: format!(
4461 "branch '{}' for table at '{}': {}",
4462 request.name, table_uri, message
4463 ),
4464 }
4465 .into(),
4466 other => Self::map_branch_error(other, &request.name, &table_uri),
4467 })?;
4468
4469 Ok(DeleteTableBranchResponse {
4470 transaction_id: None,
4471 })
4472 }
4473
4474 fn namespace_id(&self) -> String {
4475 format!("DirectoryNamespace {{ root: {:?} }}", self.root)
4476 }
4477}
4478
4479#[cfg(test)]
4480mod tests {
4481 use super::*;
4482 use arrow_ipc::reader::{FileReader, StreamReader};
4483 use lance::dataset::Dataset;
4484 use lance::index::DatasetIndexExt;
4485 use lance_core::utils::tempfile::{TempStdDir, TempStrDir};
4486 use lance_core::utils::testing::CountingObjectStore;
4487 use lance_io::object_store::{providers::local::FileStoreProvider, uri_to_url};
4488 use lance_namespace::error::ErrorCode;
4489 use lance_namespace::models::{
4490 CreateTableRequest, JsonArrowDataType, JsonArrowField, JsonArrowSchema, ListTablesRequest,
4491 QueryTableRequestColumns,
4492 };
4493 use lance_namespace::schema::convert_json_arrow_schema;
4494 use std::io::Cursor;
4495 use std::sync::{
4496 Arc,
4497 atomic::{AtomicUsize, Ordering},
4498 };
4499 use url::Url;
4500
4501 fn assert_plan_contains_all(plan: &str, expected_fragments: &[&str], context: &str) {
4502 for expected_fragment in expected_fragments {
4503 assert!(
4504 plan.contains(expected_fragment),
4505 "{}. Missing fragment: '{}'. Plan:\n{}",
4506 context,
4507 expected_fragment,
4508 plan
4509 );
4510 }
4511 }
4512
4513 async fn create_test_namespace() -> (DirectoryNamespace, TempStdDir) {
4515 let temp_dir = TempStdDir::default();
4516
4517 let namespace = DirectoryNamespaceBuilder::new(temp_dir.to_str().unwrap())
4518 .build()
4519 .await
4520 .unwrap();
4521 (namespace, temp_dir)
4522 }
4523
4524 #[derive(Debug)]
4525 struct CountingFileStoreProvider {
4526 listing_count: Arc<AtomicUsize>,
4527 }
4528
4529 #[async_trait]
4530 impl lance_io::object_store::ObjectStoreProvider for CountingFileStoreProvider {
4531 async fn new_store(
4532 &self,
4533 base_path: Url,
4534 params: &ObjectStoreParams,
4535 ) -> Result<ObjectStore> {
4536 let provider = FileStoreProvider;
4537 let mut store = provider.new_store(base_path, params).await?;
4538 store.inner = Arc::new(CountingObjectStore::new(
4539 store.inner.clone(),
4540 self.listing_count.clone(),
4541 ));
4542 Ok(store)
4543 }
4544
4545 fn extract_path(&self, url: &Url) -> Result<Path> {
4546 let provider = FileStoreProvider;
4547 provider.extract_path(url)
4548 }
4549
4550 fn calculate_object_store_prefix(
4551 &self,
4552 url: &Url,
4553 storage_options: Option<&HashMap<String, String>>,
4554 ) -> Result<String> {
4555 let provider = FileStoreProvider;
4556 provider.calculate_object_store_prefix(url, storage_options)
4557 }
4558 }
4559
4560 fn file_object_store_uri(path: &str) -> String {
4561 let file_url = uri_to_url(path).unwrap();
4562 let mut url = Url::parse("file-object-store:///").unwrap();
4563 url.set_path(file_url.path());
4564 url.to_string()
4565 }
4566
4567 fn build_listing_counting_session(listing_count: Arc<AtomicUsize>) -> Arc<Session> {
4568 let registry = Arc::new(ObjectStoreRegistry::default());
4569 registry.insert(
4570 "file-object-store",
4571 Arc::new(CountingFileStoreProvider { listing_count }),
4572 );
4573 Arc::new(Session::new(0, 0, registry))
4574 }
4575
4576 fn create_test_ipc_data(schema: &JsonArrowSchema) -> Vec<u8> {
4578 use arrow::ipc::writer::StreamWriter;
4579
4580 let arrow_schema = convert_json_arrow_schema(schema).unwrap();
4581 let arrow_schema = Arc::new(arrow_schema);
4582 let batch = arrow::record_batch::RecordBatch::new_empty(arrow_schema.clone());
4583 let mut buffer = Vec::new();
4584 {
4585 let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
4586 writer.write(&batch).unwrap();
4587 writer.finish().unwrap();
4588 }
4589 buffer
4590 }
4591
4592 fn create_ipc_data_from_batches(
4593 schema: Arc<arrow_schema::Schema>,
4594 batches: Vec<arrow::record_batch::RecordBatch>,
4595 ) -> Vec<u8> {
4596 use arrow::ipc::writer::StreamWriter;
4597
4598 let mut buffer = Vec::new();
4599 {
4600 let mut writer = StreamWriter::try_new(&mut buffer, &schema).unwrap();
4601 for batch in &batches {
4602 writer.write(batch).unwrap();
4603 }
4604 writer.finish().unwrap();
4605 }
4606 buffer
4607 }
4608
4609 fn create_non_empty_test_ipc_data() -> Vec<u8> {
4610 use arrow::array::{Int32Array, StringArray};
4611 use arrow::record_batch::RecordBatch;
4612
4613 let schema = Arc::new(convert_json_arrow_schema(&create_test_schema()).unwrap());
4614 let batch = RecordBatch::try_new(
4615 schema.clone(),
4616 vec![
4617 Arc::new(Int32Array::from(vec![1, 2])),
4618 Arc::new(StringArray::from(vec![Some("alice"), Some("bob")])),
4619 ],
4620 )
4621 .unwrap();
4622 create_ipc_data_from_batches(schema, vec![batch])
4623 }
4624
4625 fn create_single_row_test_ipc_data() -> Vec<u8> {
4626 use arrow::array::{Int32Array, StringArray};
4627 use arrow::record_batch::RecordBatch;
4628
4629 let schema = Arc::new(convert_json_arrow_schema(&create_test_schema()).unwrap());
4630 let batch = RecordBatch::try_new(
4631 schema.clone(),
4632 vec![
4633 Arc::new(Int32Array::from(vec![10])),
4634 Arc::new(StringArray::from(vec![Some("carol")])),
4635 ],
4636 )
4637 .unwrap();
4638 create_ipc_data_from_batches(schema, vec![batch])
4639 }
4640
4641 fn create_test_schema() -> JsonArrowSchema {
4643 let int_type = JsonArrowDataType::new("int32".to_string());
4644 let string_type = JsonArrowDataType::new("utf8".to_string());
4645
4646 let id_field = JsonArrowField {
4647 name: "id".to_string(),
4648 r#type: Box::new(int_type),
4649 nullable: false,
4650 metadata: None,
4651 };
4652
4653 let name_field = JsonArrowField {
4654 name: "name".to_string(),
4655 r#type: Box::new(string_type),
4656 nullable: true,
4657 metadata: None,
4658 };
4659
4660 JsonArrowSchema {
4661 fields: vec![id_field, name_field],
4662 metadata: None,
4663 }
4664 }
4665
4666 fn create_scalar_table_ipc_data() -> Vec<u8> {
4667 use arrow::array::{Int32Array, StringArray};
4668 use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
4669
4670 let schema = Arc::new(ArrowSchema::new(vec![
4671 Field::new("id", DataType::Int32, false),
4672 Field::new("name", DataType::Utf8, true),
4673 ]));
4674 let batch = arrow::record_batch::RecordBatch::try_new(
4675 schema.clone(),
4676 vec![
4677 Arc::new(Int32Array::from(vec![1, 2, 3])),
4678 Arc::new(StringArray::from(vec!["alice", "bob", "cory"])),
4679 ],
4680 )
4681 .unwrap();
4682 create_ipc_data_from_batches(schema, vec![batch])
4683 }
4684
4685 fn create_vector_table_ipc_data() -> Vec<u8> {
4686 use arrow::array::{FixedSizeListArray, Float32Array, Int32Array};
4687 use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
4688
4689 let schema = Arc::new(ArrowSchema::new(vec![
4690 Field::new("id", DataType::Int32, false),
4691 Field::new(
4692 "vector",
4693 DataType::FixedSizeList(Arc::new(Field::new("item", DataType::Float32, true)), 2),
4694 true,
4695 ),
4696 ]));
4697 let vector_field = Arc::new(Field::new("item", DataType::Float32, true));
4698 let vectors = FixedSizeListArray::try_new(
4699 vector_field,
4700 2,
4701 Arc::new(Float32Array::from(vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6])),
4702 None,
4703 )
4704 .unwrap();
4705 let batch = arrow::record_batch::RecordBatch::try_new(
4706 schema.clone(),
4707 vec![Arc::new(Int32Array::from(vec![1, 2, 3])), Arc::new(vectors)],
4708 )
4709 .unwrap();
4710 create_ipc_data_from_batches(schema, vec![batch])
4711 }
4712
4713 async fn create_scalar_table(namespace: &DirectoryNamespace, table_name: &str) {
4714 let mut create_table_request = CreateTableRequest::new();
4715 create_table_request.id = Some(vec![table_name.to_string()]);
4716 namespace
4717 .create_table(
4718 create_table_request,
4719 Bytes::from(create_scalar_table_ipc_data()),
4720 )
4721 .await
4722 .unwrap();
4723 }
4724
4725 async fn create_vector_table(namespace: &DirectoryNamespace, table_name: &str) {
4726 let mut create_table_request = CreateTableRequest::new();
4727 create_table_request.id = Some(vec![table_name.to_string()]);
4728 namespace
4729 .create_table(
4730 create_table_request,
4731 Bytes::from(create_vector_table_ipc_data()),
4732 )
4733 .await
4734 .unwrap();
4735 }
4736
4737 async fn open_dataset(namespace: &DirectoryNamespace, table_name: &str) -> Dataset {
4738 let mut describe_request = DescribeTableRequest::new();
4739 describe_request.id = Some(vec![table_name.to_string()]);
4740 let table_uri = namespace
4741 .describe_table(describe_request)
4742 .await
4743 .unwrap()
4744 .location
4745 .expect("table location should exist");
4746 Dataset::open(&table_uri).await.unwrap()
4747 }
4748
4749 async fn create_scalar_index(
4750 namespace: &DirectoryNamespace,
4751 table_name: &str,
4752 index_name: &str,
4753 ) -> Option<String> {
4754 use lance_namespace::models::CreateTableIndexRequest;
4755
4756 let mut create_index_request =
4757 CreateTableIndexRequest::new("id".to_string(), "BTREE".to_string());
4758 create_index_request.id = Some(vec![table_name.to_string()]);
4759 create_index_request.name = Some(index_name.to_string());
4760 namespace
4761 .create_table_scalar_index(create_index_request)
4762 .await
4763 .unwrap()
4764 .transaction_id
4765 }
4766
4767 async fn create_branch_with_commits(
4772 namespace: &DirectoryNamespace,
4773 table_name: &str,
4774 branch_name: &str,
4775 extra_versions: usize,
4776 ) -> String {
4777 let mut main = open_dataset(namespace, table_name).await;
4778 let fork_version = main.version().version;
4779 let branch = main
4780 .create_branch(branch_name, fork_version, None)
4781 .await
4782 .unwrap();
4783 let branch_uri = branch.uri().to_string();
4784 for i in 0..extra_versions {
4785 append_scalar_version(&branch_uri, (i as i32 + 1) * 100).await;
4786 }
4787 branch_uri
4788 }
4789
4790 async fn append_scalar_version(uri: &str, seed: i32) {
4793 use arrow::array::{Int32Array, StringArray};
4794 use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
4795 let schema = Arc::new(ArrowSchema::new(vec![
4796 Field::new("id", DataType::Int32, false),
4797 Field::new("name", DataType::Utf8, true),
4798 ]));
4799 let batch = arrow::record_batch::RecordBatch::try_new(
4800 schema.clone(),
4801 vec![
4802 Arc::new(Int32Array::from(vec![seed, seed + 1])),
4803 Arc::new(StringArray::from(vec![Some("x"), Some("y")])),
4804 ],
4805 )
4806 .unwrap();
4807 let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
4808 Dataset::write(
4809 reader,
4810 uri,
4811 Some(WriteParams {
4812 mode: WriteMode::Append,
4813 ..Default::default()
4814 }),
4815 )
4816 .await
4817 .unwrap();
4818 }
4819
4820 async fn list_versions(
4822 namespace: &DirectoryNamespace,
4823 table_name: &str,
4824 branch: Option<&str>,
4825 ) -> Result<Vec<TableVersion>> {
4826 let req = ListTableVersionsRequest {
4827 id: Some(vec![table_name.to_string()]),
4828 branch: branch.map(|b| b.to_string()),
4829 ..Default::default()
4830 };
4831 namespace.list_table_versions(req).await.map(|r| r.versions)
4832 }
4833
4834 #[tokio::test]
4835 async fn test_list_table_versions_on_branch() {
4836 let (namespace, _temp_dir) = create_test_namespace().await;
4837 create_scalar_table(&namespace, "users").await;
4838 create_branch_with_commits(&namespace, "users", "exp", 2).await;
4839
4840 let branch_versions = list_versions(&namespace, "users", Some("exp"))
4843 .await
4844 .unwrap();
4845 assert!(branch_versions.len() >= 2);
4846 assert!(
4847 branch_versions
4848 .iter()
4849 .all(|v| v.manifest_path.contains("tree/exp")),
4850 "branch versions must resolve to branch manifests: {:?}",
4851 branch_versions
4852 );
4853
4854 let main_versions = list_versions(&namespace, "users", None).await.unwrap();
4856 let main_explicit = list_versions(&namespace, "users", Some("main"))
4857 .await
4858 .unwrap();
4859 assert_eq!(main_versions.len(), main_explicit.len());
4860 assert!(
4861 main_versions
4862 .iter()
4863 .all(|v| !v.manifest_path.contains("tree/"))
4864 );
4865
4866 let missing = list_versions(&namespace, "users", Some("does-not-exist")).await;
4868 assert!(missing.is_err());
4869 assert!(missing.unwrap_err().to_string().contains("not found"));
4870 }
4871
4872 #[tokio::test]
4873 async fn test_describe_table_version_on_branch() {
4874 let (namespace, _temp_dir) = create_test_namespace().await;
4875 create_scalar_table(&namespace, "users").await;
4876 create_branch_with_commits(&namespace, "users", "exp", 2).await;
4877
4878 let branch_versions = list_versions(&namespace, "users", Some("exp"))
4879 .await
4880 .unwrap();
4881 let latest = branch_versions.iter().map(|v| v.version).max().unwrap();
4882
4883 let req = DescribeTableVersionRequest {
4885 id: Some(vec!["users".to_string()]),
4886 branch: Some("exp".to_string()),
4887 ..Default::default()
4888 };
4889 let resp = namespace.describe_table_version(req).await.unwrap();
4890 assert_eq!(resp.version.version, latest);
4891 assert!(resp.version.manifest_path.contains("tree/exp"));
4892
4893 let req = DescribeTableVersionRequest {
4895 id: Some(vec!["users".to_string()]),
4896 version: Some(latest),
4897 branch: Some("exp".to_string()),
4898 ..Default::default()
4899 };
4900 assert!(namespace.describe_table_version(req).await.is_ok());
4901
4902 let req = DescribeTableVersionRequest {
4904 id: Some(vec!["users".to_string()]),
4905 version: Some(999_999),
4906 branch: Some("exp".to_string()),
4907 ..Default::default()
4908 };
4909 assert!(namespace.describe_table_version(req).await.is_err());
4910
4911 let req = DescribeTableVersionRequest {
4913 id: Some(vec!["users".to_string()]),
4914 branch: Some("nope".to_string()),
4915 ..Default::default()
4916 };
4917 let err = namespace.describe_table_version(req).await;
4918 assert!(err.is_err() && err.unwrap_err().to_string().contains("not found"));
4919 }
4920
4921 #[tokio::test]
4922 async fn test_restore_table_on_branch() {
4923 use lance_namespace::models::RestoreTableRequest;
4924
4925 let (namespace, _temp_dir) = create_test_namespace().await;
4926 create_scalar_table(&namespace, "users").await;
4927 create_branch_with_commits(&namespace, "users", "exp", 2).await;
4928
4929 let before = list_versions(&namespace, "users", Some("exp"))
4930 .await
4931 .unwrap();
4932 let branch_latest = before.iter().map(|v| v.version).max().unwrap();
4933 let earliest = before.iter().map(|v| v.version).min().unwrap();
4934 let main_before = list_versions(&namespace, "users", None)
4935 .await
4936 .unwrap()
4937 .len();
4938
4939 let req = RestoreTableRequest {
4942 id: Some(vec!["users".to_string()]),
4943 version: earliest,
4944 branch: Some("exp".to_string()),
4945 ..Default::default()
4946 };
4947 let resp = namespace.restore_table(req).await.unwrap();
4948 assert!(resp.transaction_id.is_some());
4949
4950 let after = list_versions(&namespace, "users", Some("exp"))
4951 .await
4952 .unwrap();
4953 let new_latest = after.iter().map(|v| v.version).max().unwrap();
4954 assert!(
4955 new_latest > branch_latest,
4956 "restore should add a branch version"
4957 );
4958
4959 let main_after = list_versions(&namespace, "users", None)
4960 .await
4961 .unwrap()
4962 .len();
4963 assert_eq!(main_after, main_before, "main must be unaffected");
4964 }
4965
4966 #[tokio::test]
4967 async fn test_batch_delete_table_versions_on_branch() {
4968 use lance_namespace::models::{BatchDeleteTableVersionsRequest, VersionRange};
4969
4970 let (namespace, _temp_dir) = create_test_namespace().await;
4971 create_scalar_table(&namespace, "users").await;
4972 create_branch_with_commits(&namespace, "users", "exp", 2).await;
4973
4974 let before = list_versions(&namespace, "users", Some("exp"))
4975 .await
4976 .unwrap();
4977 let main_before = list_versions(&namespace, "users", None).await.unwrap();
4978
4979 let req = BatchDeleteTableVersionsRequest {
4984 id: Some(vec!["users".to_string()]),
4985 branch: Some("exp".to_string()),
4986 ranges: vec![VersionRange::new(0, -1)],
4987 ..Default::default()
4988 };
4989 let resp = namespace.batch_delete_table_versions(req).await.unwrap();
4990 assert_eq!(
4991 resp.deleted_count,
4992 Some(before.len() as i64),
4993 "every branch manifest should be physically deleted"
4994 );
4995
4996 assert!(
4998 list_versions(&namespace, "users", Some("exp"))
4999 .await
5000 .is_err()
5001 );
5002 let main_after = list_versions(&namespace, "users", None).await.unwrap();
5003 assert_eq!(
5004 main_after.len(),
5005 main_before.len(),
5006 "main must be untouched"
5007 );
5008 }
5009
5010 #[tokio::test]
5011 async fn test_create_table_version_on_branch() {
5012 use futures::TryStreamExt;
5013 use lance_namespace::models::CreateTableVersionRequest;
5014
5015 let (namespace, _temp_dir) = create_test_namespace().await;
5016 create_scalar_table(&namespace, "users").await;
5017 let branch_uri = create_branch_with_commits(&namespace, "users", "exp", 1).await;
5018
5019 let branch_ds = Dataset::open(&branch_uri).await.unwrap();
5021 let versions_dir = branch_ds.versions_dir();
5022 let store = branch_ds.object_store(None).await.unwrap();
5023 let existing = store
5024 .inner
5025 .list(Some(&versions_dir))
5026 .try_collect::<Vec<_>>()
5027 .await
5028 .unwrap()
5029 .into_iter()
5030 .find(|m| {
5031 m.location
5032 .filename()
5033 .map(|f| f.ends_with(".manifest"))
5034 .unwrap_or(false)
5035 })
5036 .expect("a branch manifest");
5037 let bytes = store
5038 .inner
5039 .get(&existing.location)
5040 .await
5041 .unwrap()
5042 .bytes()
5043 .await
5044 .unwrap();
5045 let staging = versions_dir.join("staging_manifest");
5046 store.inner.put(&staging, bytes.into()).await.unwrap();
5047
5048 let main_before = list_versions(&namespace, "users", None)
5049 .await
5050 .unwrap()
5051 .len();
5052 let new_version = list_versions(&namespace, "users", Some("exp"))
5053 .await
5054 .unwrap()
5055 .iter()
5056 .map(|v| v.version)
5057 .max()
5058 .unwrap()
5059 + 1;
5060
5061 let req = CreateTableVersionRequest {
5062 id: Some(vec!["users".to_string()]),
5063 version: new_version,
5064 manifest_path: staging.to_string(),
5065 naming_scheme: Some("V2".to_string()),
5066 branch: Some("exp".to_string()),
5067 ..Default::default()
5068 };
5069 let resp = namespace.create_table_version(req).await.unwrap();
5070 let info = resp.version.expect("version info");
5071 assert!(
5073 info.manifest_path.contains("tree/exp"),
5074 "got {}",
5075 info.manifest_path
5076 );
5077
5078 let after = list_versions(&namespace, "users", Some("exp"))
5080 .await
5081 .unwrap();
5082 assert!(after.iter().any(|v| v.version == new_version));
5083 let main_after = list_versions(&namespace, "users", None)
5084 .await
5085 .unwrap()
5086 .len();
5087 assert_eq!(main_after, main_before, "main must be unaffected");
5088 }
5089
5090 #[tokio::test]
5095 async fn test_external_manifest_store_resolves_branch_from_base_path() {
5096 use futures::TryStreamExt;
5097 use lance::io::commit::namespace_manifest::LanceNamespaceExternalManifestStore;
5098 use lance_table::io::commit::external_manifest::ExternalManifestStore;
5099
5100 let (namespace, _temp_dir) = create_test_namespace().await;
5101 create_scalar_table(&namespace, "users").await; let branch_uri = create_branch_with_commits(&namespace, "users", "exp", 2).await;
5103
5104 let namespace = Arc::new(namespace);
5105 let table_id = vec!["users".to_string()];
5106 let branch_ds = Dataset::open(&branch_uri).await.unwrap();
5107 let branch_base = branch_ds.branch_location().path;
5108 let root_base = branch_ds.branch_location().find_main().unwrap().path;
5109 let store = LanceNamespaceExternalManifestStore::new(
5110 namespace.clone(),
5111 table_id.clone(),
5112 root_base.clone(),
5113 );
5114
5115 let (branch_latest, branch_path) = store
5118 .get_latest_version(branch_base.as_ref())
5119 .await
5120 .unwrap()
5121 .expect("branch has versions");
5122 let (_main_latest, main_path) = store
5123 .get_latest_version(root_base.as_ref())
5124 .await
5125 .unwrap()
5126 .expect("main has versions");
5127 assert!(
5128 branch_path.contains("tree/exp"),
5129 "branch latest must resolve to the branch tree: {}",
5130 branch_path
5131 );
5132 assert!(
5133 !main_path.contains("tree/exp"),
5134 "main latest must not resolve to a branch tree: {}",
5135 main_path
5136 );
5137
5138 let described = store
5140 .get(branch_base.as_ref(), branch_latest)
5141 .await
5142 .unwrap();
5143 assert!(
5144 described.contains("tree/exp"),
5145 "describe on the branch must resolve to the branch tree: {}",
5146 described
5147 );
5148
5149 assert!(store.get_latest_version("somewhere/else").await.is_err());
5151
5152 let versions_dir = branch_ds.versions_dir();
5155 let obj = branch_ds.object_store(None).await.unwrap();
5156 let existing = obj
5157 .inner
5158 .list(Some(&versions_dir))
5159 .try_collect::<Vec<_>>()
5160 .await
5161 .unwrap()
5162 .into_iter()
5163 .find(|m| {
5164 m.location
5165 .filename()
5166 .map(|f| f.ends_with(".manifest"))
5167 .unwrap_or(false)
5168 })
5169 .expect("a branch manifest");
5170 let bytes = obj
5171 .inner
5172 .get(&existing.location)
5173 .await
5174 .unwrap()
5175 .bytes()
5176 .await
5177 .unwrap();
5178 let size = bytes.len() as u64;
5179 let staging = versions_dir.clone().join("staging_manifest");
5180 obj.inner.put(&staging, bytes.into()).await.unwrap();
5181
5182 let committed = store
5183 .put(
5184 &branch_base,
5185 branch_latest + 1,
5186 &staging,
5187 size,
5188 None,
5189 obj.inner.as_ref(),
5190 ManifestNamingScheme::V2,
5191 )
5192 .await
5193 .unwrap();
5194 assert!(
5195 committed.path.to_string().contains("tree/exp"),
5196 "a commit through a branch-qualified base must land on the branch tree: {}",
5197 committed.path
5198 );
5199 }
5200
5201 #[tokio::test]
5209 async fn test_write_into_namespace_on_branch_appends_to_branch() {
5210 use lance::dataset::builder::DatasetBuilder;
5211 use lance_namespace::models::CreateTableBranchRequest;
5212
5213 let temp = TempStdDir::default();
5214 let namespace = Arc::new(
5215 DirectoryNamespaceBuilder::new(temp.to_str().unwrap())
5216 .manifest_enabled(true)
5217 .table_version_tracking_enabled(true)
5218 .ops_metrics_enabled(true)
5219 .build()
5220 .await
5221 .unwrap(),
5222 );
5223 let ns: Arc<dyn LanceNamespace> = namespace.clone();
5224 let table_id = vec!["t".to_string()];
5225 create_managed_table(&ns, &table_id).await; ns.create_table_branch(CreateTableBranchRequest {
5227 id: Some(table_id.clone()),
5228 name: "exp".to_string(),
5229 ..Default::default()
5230 })
5231 .await
5232 .unwrap();
5233
5234 let main_chain_len = |ns: Arc<dyn LanceNamespace>, table_id: Vec<String>| async move {
5235 ns.list_table_versions(ListTableVersionsRequest {
5236 id: Some(table_id),
5237 ..Default::default()
5238 })
5239 .await
5240 .unwrap()
5241 .versions
5242 .len()
5243 };
5244 let main_before = main_chain_len(ns.clone(), table_id.clone()).await;
5245 let commits_before = namespace
5246 .retrieve_ops_metrics()
5247 .get("create_table_version")
5248 .copied()
5249 .unwrap_or(0);
5250
5251 let branch_ds = Dataset::write_into_namespace_on_branch(
5252 RecordBatchIterator::new(vec![Ok(single_int_batch(3))], single_int_schema()),
5253 ns.clone(),
5254 table_id.clone(),
5255 "exp",
5256 Some(WriteParams {
5257 mode: WriteMode::Append,
5258 ..Default::default()
5259 }),
5260 )
5261 .await
5262 .unwrap();
5263 assert_eq!(branch_ds.manifest.branch.as_deref(), Some("exp"));
5264 assert_eq!(scan_id_column(&branch_ds).await, vec![1, 2, 3]);
5265
5266 let commits_after = namespace
5269 .retrieve_ops_metrics()
5270 .get("create_table_version")
5271 .copied()
5272 .unwrap_or(0);
5273 assert_eq!(
5274 commits_after,
5275 commits_before + 1,
5276 "the branch append must register its version via create_table_version"
5277 );
5278 let exp_versions = ns
5279 .list_table_versions(ListTableVersionsRequest {
5280 id: Some(table_id.clone()),
5281 branch: Some("exp".to_string()),
5282 ..Default::default()
5283 })
5284 .await
5285 .unwrap()
5286 .versions;
5287 assert!(
5288 exp_versions
5289 .iter()
5290 .all(|v| v.manifest_path.contains("tree/exp")),
5291 "branch versions must resolve to the branch tree: {:?}",
5292 exp_versions
5293 );
5294 assert_eq!(
5295 main_chain_len(ns.clone(), table_id.clone()).await,
5296 main_before,
5297 "main's catalog must be untouched by the branch append"
5298 );
5299
5300 Dataset::write_into_namespace(
5303 RecordBatchIterator::new(vec![Ok(single_int_batch(100))], single_int_schema()),
5304 ns.clone(),
5305 table_id.clone(),
5306 Some(WriteParams {
5307 mode: WriteMode::Append,
5308 ..Default::default()
5309 }),
5310 )
5311 .await
5312 .unwrap();
5313 assert_eq!(
5314 main_chain_len(ns.clone(), table_id.clone()).await,
5315 main_before + 1,
5316 "a managed main append must register its version in the catalog"
5317 );
5318 let fresh = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
5319 .await
5320 .unwrap()
5321 .load()
5322 .await
5323 .unwrap();
5324 assert_eq!(
5325 scan_id_column(&fresh).await,
5326 vec![1, 2, 100],
5327 "a fresh managed open must resolve the appended version, not a stale latest"
5328 );
5329 }
5330
5331 #[tokio::test]
5333 async fn test_write_into_namespace_on_branch_rejects_create() {
5334 use arrow::array::{Int32Array, StringArray};
5335 use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
5336
5337 let (namespace, _temp_dir) = create_test_namespace().await;
5338 let namespace = Arc::new(namespace);
5339
5340 let schema = Arc::new(ArrowSchema::new(vec![
5341 Field::new("id", DataType::Int32, false),
5342 Field::new("name", DataType::Utf8, true),
5343 ]));
5344 let batch = arrow::record_batch::RecordBatch::try_new(
5345 schema.clone(),
5346 vec![
5347 Arc::new(Int32Array::from(vec![1])),
5348 Arc::new(StringArray::from(vec![Some("a")])),
5349 ],
5350 )
5351 .unwrap();
5352 let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
5353
5354 let result = Dataset::write_into_namespace_on_branch(
5355 reader,
5356 namespace.clone(),
5357 vec!["new_table".to_string()],
5358 "exp",
5359 Some(WriteParams {
5360 mode: WriteMode::Create,
5361 ..Default::default()
5362 }),
5363 )
5364 .await;
5365 assert!(result.is_err(), "create on a branch must be rejected");
5366 assert!(
5367 result.unwrap_err().to_string().contains("branch"),
5368 "error should mention the branch restriction"
5369 );
5370 }
5371
5372 #[tokio::test]
5373 async fn test_branch_name_validation_rejects_traversal() {
5374 let (namespace, _temp_dir) = create_test_namespace().await;
5375 create_scalar_table(&namespace, "users").await;
5376
5377 let err = list_versions(&namespace, "users", Some("../evil")).await;
5380 assert!(err.is_err());
5381 assert!(err.unwrap_err().to_string().contains("invalid branch name"));
5382 }
5383
5384 #[tokio::test]
5385 async fn test_branch_ops_reject_zombie_branch() {
5386 use futures::TryStreamExt;
5387 use lance_namespace::models::{
5388 BatchDeleteTableVersionsRequest, CreateTableVersionRequest, RestoreTableRequest,
5389 VersionRange,
5390 };
5391
5392 let (namespace, _temp_dir) = create_test_namespace().await;
5393 create_scalar_table(&namespace, "users").await;
5394
5395 let dataset = open_dataset(&namespace, "users").await;
5398 let store = dataset.object_store(None).await.unwrap();
5399 let manifest = store
5400 .inner
5401 .list(Some(&dataset.versions_dir()))
5402 .try_collect::<Vec<_>>()
5403 .await
5404 .unwrap()
5405 .into_iter()
5406 .find(|m| {
5407 m.location
5408 .filename()
5409 .map(|f| f.ends_with(".manifest"))
5410 .unwrap_or(false)
5411 })
5412 .expect("a manifest");
5413 let bytes = store
5414 .inner
5415 .get(&manifest.location)
5416 .await
5417 .unwrap()
5418 .bytes()
5419 .await
5420 .unwrap();
5421 let zombie = Path::from(format!(
5422 "{}/tree/ghost/_versions/{}",
5423 dataset.branch_location().path,
5424 manifest.location.filename().unwrap()
5425 ));
5426 store.inner.put(&zombie, bytes.into()).await.unwrap();
5427
5428 assert!(dataset.branches().get("ghost").await.is_err());
5431
5432 fn rejected<T: std::fmt::Debug>(label: &str, r: Result<T>) {
5433 match r {
5434 Ok(v) => panic!("{label} must reject the zombie branch, got Ok({v:?})"),
5435 Err(e) => assert!(e.to_string().contains("not found"), "{label}: {e}"),
5436 }
5437 }
5438
5439 rejected(
5440 "list",
5441 list_versions(&namespace, "users", Some("ghost")).await,
5442 );
5443 rejected(
5444 "describe",
5445 namespace
5446 .describe_table_version(DescribeTableVersionRequest {
5447 id: Some(vec!["users".to_string()]),
5448 branch: Some("ghost".to_string()),
5449 ..Default::default()
5450 })
5451 .await,
5452 );
5453 rejected(
5454 "create",
5455 namespace
5456 .create_table_version(CreateTableVersionRequest {
5457 id: Some(vec!["users".to_string()]),
5458 version: 2,
5459 manifest_path: zombie.to_string(),
5460 branch: Some("ghost".to_string()),
5461 ..Default::default()
5462 })
5463 .await,
5464 );
5465 rejected(
5466 "restore",
5467 namespace
5468 .restore_table(RestoreTableRequest {
5469 id: Some(vec!["users".to_string()]),
5470 version: 1,
5471 branch: Some("ghost".to_string()),
5472 ..Default::default()
5473 })
5474 .await,
5475 );
5476 rejected(
5477 "batch_delete",
5478 namespace
5479 .batch_delete_table_versions(BatchDeleteTableVersionsRequest {
5480 id: Some(vec!["users".to_string()]),
5481 branch: Some("ghost".to_string()),
5482 ranges: vec![VersionRange::new(1, 1)],
5483 ..Default::default()
5484 })
5485 .await,
5486 );
5487 }
5488
5489 #[tokio::test]
5494 async fn test_batch_delete_table_versions_main_v2() {
5495 use lance_namespace::models::{BatchDeleteTableVersionsRequest, VersionRange};
5496
5497 let (namespace, _temp_dir) = create_test_namespace().await;
5498 create_scalar_table(&namespace, "users").await; let main_uri = open_dataset(&namespace, "users").await.uri().to_string();
5500 append_scalar_version(&main_uri, 100).await; append_scalar_version(&main_uri, 200).await; let before = list_versions(&namespace, "users", None).await.unwrap();
5504 assert!(before.len() >= 3);
5505 assert!(
5508 before
5509 .iter()
5510 .all(|v| v.manifest_path.rsplit('/').next().unwrap().len() == 29),
5511 "expected V2-named manifests: {:?}",
5512 before
5513 );
5514 let min_v = before.iter().map(|v| v.version).min().unwrap();
5515 let max_v = before.iter().map(|v| v.version).max().unwrap();
5516
5517 let req = BatchDeleteTableVersionsRequest {
5520 id: Some(vec!["users".to_string()]),
5521 ranges: vec![VersionRange::new(min_v, max_v)],
5522 ..Default::default()
5523 };
5524 let resp = namespace.batch_delete_table_versions(req).await.unwrap();
5525 assert_eq!(
5526 resp.deleted_count,
5527 Some((before.len() - 1) as i64),
5528 "V2 manifests must actually be deleted (was 0 before the fix)"
5529 );
5530
5531 let after = list_versions(&namespace, "users", None).await.unwrap();
5532 assert_eq!(after.len(), 1);
5533 assert_eq!(after[0].version, max_v);
5534 }
5535
5536 #[tokio::test]
5538 async fn test_batch_delete_end_is_exclusive() {
5539 use lance_namespace::models::{BatchDeleteTableVersionsRequest, VersionRange};
5540
5541 let (namespace, _temp_dir) = create_test_namespace().await;
5542 create_scalar_table(&namespace, "users").await; let main_uri = open_dataset(&namespace, "users").await.uri().to_string();
5544 append_scalar_version(&main_uri, 100).await; append_scalar_version(&main_uri, 200).await; let before = list_versions(&namespace, "users", None).await.unwrap();
5548 let min_v = before.iter().map(|v| v.version).min().unwrap();
5549
5550 let req = BatchDeleteTableVersionsRequest {
5551 id: Some(vec!["users".to_string()]),
5552 ranges: vec![VersionRange::new(min_v, min_v + 1)],
5553 ..Default::default()
5554 };
5555 let resp = namespace.batch_delete_table_versions(req).await.unwrap();
5556 assert_eq!(
5557 resp.deleted_count,
5558 Some(1),
5559 "only min_v is in [min_v, min_v+1)"
5560 );
5561
5562 let after = list_versions(&namespace, "users", None).await.unwrap();
5563 assert!(
5564 !after.iter().any(|v| v.version == min_v),
5565 "min_v must be deleted"
5566 );
5567 assert_eq!(after.len(), before.len() - 1, "exactly one version removed");
5568 }
5569
5570 #[tokio::test]
5571 async fn test_batch_delete_rejects_unbounded_range() {
5572 use lance_namespace::models::{BatchDeleteTableVersionsRequest, VersionRange};
5573
5574 let (namespace, _temp_dir) = create_test_namespace().await;
5575 create_scalar_table(&namespace, "users").await;
5576
5577 let req = BatchDeleteTableVersionsRequest {
5580 id: Some(vec!["users".to_string()]),
5581 ranges: vec![VersionRange::new(0, i64::MAX)],
5582 ..Default::default()
5583 };
5584 let err = namespace.batch_delete_table_versions(req).await;
5585 assert!(err.is_err());
5586 assert!(
5587 err.unwrap_err().to_string().contains("limit"),
5588 "expected a range-too-large error"
5589 );
5590 }
5591
5592 async fn create_managed_namespace(path: &str) -> Arc<dyn LanceNamespace> {
5594 Arc::new(
5595 DirectoryNamespaceBuilder::new(path)
5596 .manifest_enabled(true)
5597 .table_version_tracking_enabled(true)
5598 .build()
5599 .await
5600 .unwrap(),
5601 )
5602 }
5603
5604 fn single_int_schema() -> Arc<arrow::datatypes::Schema> {
5605 use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
5606 Arc::new(ArrowSchema::new(vec![Field::new(
5607 "id",
5608 DataType::Int32,
5609 false,
5610 )]))
5611 }
5612
5613 fn single_int_batch(seed: i32) -> arrow::record_batch::RecordBatch {
5614 use arrow::array::Int32Array;
5615 arrow::record_batch::RecordBatch::try_new(
5616 single_int_schema(),
5617 vec![Arc::new(Int32Array::from(vec![seed]))],
5618 )
5619 .unwrap()
5620 }
5621
5622 async fn create_managed_table(ns: &Arc<dyn LanceNamespace>, table_id: &[String]) -> Dataset {
5625 let mut ds = Dataset::write_into_namespace(
5626 RecordBatchIterator::new(vec![Ok(single_int_batch(1))], single_int_schema()),
5627 ns.clone(),
5628 table_id.to_vec(),
5629 Some(WriteParams {
5630 mode: WriteMode::Create,
5631 ..Default::default()
5632 }),
5633 )
5634 .await
5635 .unwrap();
5636 ds.append(
5637 RecordBatchIterator::new(vec![Ok(single_int_batch(2))], single_int_schema()),
5638 None,
5639 )
5640 .await
5641 .unwrap();
5642 ds
5643 }
5644
5645 async fn scan_id_column(ds: &Dataset) -> Vec<i32> {
5647 use arrow::array::Int32Array;
5648 use futures::TryStreamExt;
5649 let batches: Vec<arrow::record_batch::RecordBatch> = ds
5650 .scan()
5651 .try_into_stream()
5652 .await
5653 .unwrap()
5654 .try_collect()
5655 .await
5656 .unwrap();
5657 let mut ids: Vec<i32> = batches
5658 .iter()
5659 .flat_map(|b| {
5660 b.column(0)
5661 .as_any()
5662 .downcast_ref::<Int32Array>()
5663 .unwrap()
5664 .values()
5665 .to_vec()
5666 })
5667 .collect();
5668 ids.sort();
5669 ids
5670 }
5671
5672 #[tokio::test]
5677 async fn test_managed_branch_open_and_commit() {
5678 use futures::TryStreamExt;
5679 use lance::dataset::builder::DatasetBuilder;
5680 use lance_namespace::models::CreateTableBranchRequest;
5681
5682 let temp = TempStdDir::default();
5683 let ns = create_managed_namespace(temp.to_str().unwrap()).await;
5684 let table_id = vec!["t".to_string()];
5685 create_managed_table(&ns, &table_id).await;
5686 let main_before = ns
5687 .list_table_versions(ListTableVersionsRequest {
5688 id: Some(table_id.clone()),
5689 ..Default::default()
5690 })
5691 .await
5692 .unwrap()
5693 .versions
5694 .len();
5695
5696 ns.create_table_branch(CreateTableBranchRequest {
5699 id: Some(table_id.clone()),
5700 name: "exp".to_string(),
5701 ..Default::default()
5702 })
5703 .await
5704 .unwrap();
5705
5706 let mut branch_ds = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
5709 .await
5710 .unwrap()
5711 .with_branch("exp", None)
5712 .load()
5713 .await
5714 .unwrap();
5715 assert_eq!(
5716 branch_ds.manifest.branch.as_deref(),
5717 Some("exp"),
5718 "with_branch on a managed table must open the branch chain"
5719 );
5720 let branch_base = branch_ds.branch_location().path;
5721 assert!(
5722 branch_base.as_ref().ends_with("tree/exp"),
5723 "the branch dataset must be rooted at the branch chain: {}",
5724 branch_base
5725 );
5726 let branch_v_before = branch_ds.version().version;
5727
5728 branch_ds
5730 .append(
5731 RecordBatchIterator::new(vec![Ok(single_int_batch(3))], single_int_schema()),
5732 None,
5733 )
5734 .await
5735 .unwrap();
5736 assert_eq!(
5737 branch_ds.manifest.branch.as_deref(),
5738 Some("exp"),
5739 "the commit must stay on the branch"
5740 );
5741 assert!(
5742 branch_ds.version().version > branch_v_before,
5743 "the branch version must advance after the commit"
5744 );
5745 assert_eq!(scan_id_column(&branch_ds).await, vec![1, 2, 3]);
5746
5747 let store = branch_ds.object_store(None).await.unwrap();
5751 let branch_data = branch_base.clone().join("data");
5752 let branch_files = store
5753 .inner
5754 .list(Some(&branch_data))
5755 .try_collect::<Vec<_>>()
5756 .await
5757 .unwrap();
5758 assert!(
5759 !branch_files.is_empty(),
5760 "the branch commit must place data files under the branch chain"
5761 );
5762
5763 let table_uri = ns
5765 .describe_table(DescribeTableRequest {
5766 id: Some(table_id.clone()),
5767 ..Default::default()
5768 })
5769 .await
5770 .unwrap()
5771 .location
5772 .unwrap();
5773 let fs_branch_ds = DatasetBuilder::from_uri(&table_uri)
5774 .with_branch("exp", None)
5775 .load()
5776 .await
5777 .unwrap();
5778 assert_eq!(fs_branch_ds.manifest.branch.as_deref(), Some("exp"));
5779 assert_eq!(scan_id_column(&fs_branch_ds).await, vec![1, 2, 3]);
5780
5781 let main_after = ns
5784 .list_table_versions(ListTableVersionsRequest {
5785 id: Some(table_id.clone()),
5786 ..Default::default()
5787 })
5788 .await
5789 .unwrap()
5790 .versions
5791 .len();
5792 assert_eq!(
5793 main_after, main_before,
5794 "committing on the branch must not change main's chain"
5795 );
5796 let main_ds = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
5797 .await
5798 .unwrap()
5799 .load()
5800 .await
5801 .unwrap();
5802 assert_eq!(main_ds.manifest.branch, None);
5803 assert_eq!(scan_id_column(&main_ds).await, vec![1, 2]);
5804 }
5805
5806 #[tokio::test]
5811 async fn test_managed_branch_tags() {
5812 use lance::dataset::builder::DatasetBuilder;
5813 use lance::dataset::refs::Ref;
5814 use lance_namespace::models::CreateTableBranchRequest;
5815
5816 let temp = TempStdDir::default();
5817 let ns = create_managed_namespace(temp.to_str().unwrap()).await;
5818 let table_id = vec!["t".to_string()];
5819 let main_ds = create_managed_table(&ns, &table_id).await;
5820 ns.create_table_branch(CreateTableBranchRequest {
5821 id: Some(table_id.clone()),
5822 name: "exp".to_string(),
5823 ..Default::default()
5824 })
5825 .await
5826 .unwrap();
5827 let mut branch_ds = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
5828 .await
5829 .unwrap()
5830 .with_branch("exp", None)
5831 .load()
5832 .await
5833 .unwrap();
5834 branch_ds
5835 .append(
5836 RecordBatchIterator::new(vec![Ok(single_int_batch(3))], single_int_schema()),
5837 None,
5838 )
5839 .await
5840 .unwrap();
5841 let branch_version = branch_ds.version().version;
5842
5843 main_ds
5846 .tags()
5847 .create("exp-tag", ("exp", Some(branch_version)))
5848 .await
5849 .unwrap();
5850 let tag = main_ds.tags().get("exp-tag").await.unwrap();
5851 assert_eq!(tag.branch.as_deref(), Some("exp"));
5852 assert_eq!(tag.version, branch_version);
5853
5854 branch_ds
5856 .tags()
5857 .create("exp-tag2", branch_version)
5858 .await
5859 .unwrap();
5860 let tag2 = branch_ds.tags().get("exp-tag2").await.unwrap();
5861 assert_eq!(tag2.branch.as_deref(), Some("exp"));
5862
5863 let tag_open = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
5866 .await
5867 .unwrap()
5868 .with_tag("exp-tag")
5869 .load()
5870 .await
5871 .unwrap();
5872 assert_eq!(tag_open.manifest.branch.as_deref(), Some("exp"));
5873 assert_eq!(tag_open.version().version, branch_version);
5874 assert_eq!(scan_id_column(&tag_open).await, vec![1, 2, 3]);
5875
5876 let tag_checkout = main_ds
5878 .checkout_version(Ref::Tag("exp-tag".to_string()))
5879 .await
5880 .unwrap();
5881 assert_eq!(tag_checkout.manifest.branch.as_deref(), Some("exp"));
5882 assert_eq!(scan_id_column(&tag_checkout).await, vec![1, 2, 3]);
5883
5884 let err = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
5886 .await
5887 .unwrap()
5888 .with_tag("no-such-tag")
5889 .load()
5890 .await;
5891 assert!(err.is_err(), "a missing tag must error");
5892 }
5893
5894 #[tokio::test]
5899 async fn test_managed_cross_branch_checkout() {
5900 use lance::dataset::builder::DatasetBuilder;
5901 use lance::dataset::refs::Ref;
5902 use lance_namespace::models::CreateTableBranchRequest;
5903
5904 let temp = TempStdDir::default();
5905 let ns = create_managed_namespace(temp.to_str().unwrap()).await;
5906 let table_id = vec!["t".to_string()];
5907 let mut main_ds = create_managed_table(&ns, &table_id).await;
5908 ns.create_table_branch(CreateTableBranchRequest {
5909 id: Some(table_id.clone()),
5910 name: "exp".to_string(),
5911 ..Default::default()
5912 })
5913 .await
5914 .unwrap();
5915
5916 let mut branch_ds = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
5918 .await
5919 .unwrap()
5920 .with_branch("exp", None)
5921 .load()
5922 .await
5923 .unwrap();
5924 branch_ds
5925 .append(
5926 RecordBatchIterator::new(vec![Ok(single_int_batch(3))], single_int_schema()),
5927 None,
5928 )
5929 .await
5930 .unwrap();
5931 let overlap_version = branch_ds.version().version;
5932 while main_ds.version().version < overlap_version {
5933 main_ds
5934 .append(
5935 RecordBatchIterator::new(vec![Ok(single_int_batch(100))], single_int_schema()),
5936 None,
5937 )
5938 .await
5939 .unwrap();
5940 }
5941
5942 let on_branch = main_ds
5945 .checkout_version(Ref::Version(Some("exp".to_string()), Some(overlap_version)))
5946 .await
5947 .unwrap();
5948 assert_eq!(on_branch.manifest.branch.as_deref(), Some("exp"));
5949 assert_eq!(scan_id_column(&on_branch).await, vec![1, 2, 3]);
5950
5951 let mut on_branch_latest = main_ds.checkout_branch("exp").await.unwrap();
5953 assert_eq!(on_branch_latest.manifest.branch.as_deref(), Some("exp"));
5954 assert_eq!(on_branch_latest.version().version, overlap_version);
5955
5956 let main_chain_len = |ns: Arc<dyn LanceNamespace>, table_id: Vec<String>| async move {
5959 ns.list_table_versions(ListTableVersionsRequest {
5960 id: Some(table_id),
5961 ..Default::default()
5962 })
5963 .await
5964 .unwrap()
5965 .versions
5966 .len()
5967 };
5968 let main_before = main_chain_len(ns.clone(), table_id.clone()).await;
5969 on_branch_latest
5970 .append(
5971 RecordBatchIterator::new(vec![Ok(single_int_batch(4))], single_int_schema()),
5972 None,
5973 )
5974 .await
5975 .unwrap();
5976 assert_eq!(on_branch_latest.manifest.branch.as_deref(), Some("exp"));
5977 assert_eq!(scan_id_column(&on_branch_latest).await, vec![1, 2, 3, 4]);
5978 assert_eq!(
5979 main_chain_len(ns.clone(), table_id.clone()).await,
5980 main_before,
5981 "a commit on the checked-out branch must not advance main's chain"
5982 );
5983
5984 let on_main = branch_ds
5986 .checkout_version(Ref::Version(None, Some(1)))
5987 .await
5988 .unwrap();
5989 assert_eq!(on_main.manifest.branch, None);
5990 assert_eq!(scan_id_column(&on_main).await, vec![1]);
5991
5992 ns.create_table_branch(CreateTableBranchRequest {
5994 id: Some(table_id.clone()),
5995 name: "exp2".to_string(),
5996 ..Default::default()
5997 })
5998 .await
5999 .unwrap();
6000 let on_branch2 = branch_ds.checkout_branch("exp2").await.unwrap();
6001 assert_eq!(on_branch2.manifest.branch.as_deref(), Some("exp2"));
6002
6003 let err = main_ds
6005 .checkout_version(Ref::Version(Some("exp".to_string()), Some(999)))
6006 .await;
6007 assert!(err.is_err(), "a version missing from the branch must error");
6008 }
6009
6010 #[tokio::test]
6016 async fn test_commit_builder_honors_explicit_handler_for_dataset_dest() {
6017 use lance::dataset::write::{CommitBuilder, InsertBuilder};
6018 use lance::dataset::{WriteDestination, builder::DatasetBuilder};
6019 use lance::io::commit::namespace_manifest::LanceNamespaceExternalManifestStore;
6020 use lance_table::io::commit::external_manifest::ExternalManifestCommitHandler;
6021
6022 let temp = TempStdDir::default();
6023 let namespace = Arc::new(
6024 DirectoryNamespaceBuilder::new(temp.to_str().unwrap())
6025 .manifest_enabled(true)
6026 .table_version_tracking_enabled(true)
6027 .ops_metrics_enabled(true)
6028 .build()
6029 .await
6030 .unwrap(),
6031 );
6032 let ns: Arc<dyn LanceNamespace> = namespace.clone();
6033 let table_id = vec!["t".to_string()];
6034 create_managed_table(&ns, &table_id).await; let table_uri = ns
6038 .describe_table(DescribeTableRequest {
6039 id: Some(table_id.clone()),
6040 ..Default::default()
6041 })
6042 .await
6043 .unwrap()
6044 .location
6045 .unwrap();
6046 let plain_ds = Arc::new(Dataset::open(&table_uri).await.unwrap());
6047
6048 let transaction = InsertBuilder::new(WriteDestination::Dataset(plain_ds.clone()))
6049 .with_params(&WriteParams {
6050 mode: WriteMode::Append,
6051 ..Default::default()
6052 })
6053 .execute_uncommitted(vec![single_int_batch(3)])
6054 .await
6055 .unwrap();
6056
6057 let handler = Arc::new(ExternalManifestCommitHandler {
6058 external_manifest_store: Arc::new(
6059 LanceNamespaceExternalManifestStore::for_table_uri(
6060 ns.clone(),
6061 table_id.clone(),
6062 &table_uri,
6063 )
6064 .unwrap(),
6065 ),
6066 });
6067 let commits_before = namespace
6068 .retrieve_ops_metrics()
6069 .get("create_table_version")
6070 .copied()
6071 .unwrap_or(0);
6072 let committed = CommitBuilder::new(WriteDestination::Dataset(plain_ds))
6073 .with_commit_handler(handler)
6074 .execute(transaction)
6075 .await
6076 .unwrap();
6077 assert_eq!(scan_id_column(&committed).await, vec![1, 2, 3]);
6078
6079 let commits_after = namespace
6080 .retrieve_ops_metrics()
6081 .get("create_table_version")
6082 .copied()
6083 .unwrap_or(0);
6084 assert_eq!(
6085 commits_after,
6086 commits_before + 1,
6087 "the explicit handler must route the commit through create_table_version"
6088 );
6089 let fresh = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
6090 .await
6091 .unwrap()
6092 .load()
6093 .await
6094 .unwrap();
6095 assert_eq!(
6096 scan_id_column(&fresh).await,
6097 vec![1, 2, 3],
6098 "a fresh managed open must resolve the committed version"
6099 );
6100 }
6101
6102 #[tokio::test]
6104 async fn test_managed_branch_from_non_latest_fork() {
6105 use lance::dataset::builder::DatasetBuilder;
6106 use lance_namespace::models::CreateTableBranchRequest;
6107
6108 let temp = TempStdDir::default();
6109 let ns = create_managed_namespace(temp.to_str().unwrap()).await;
6110 let table_id = vec!["t".to_string()];
6111 create_managed_table(&ns, &table_id).await; ns.create_table_branch(CreateTableBranchRequest {
6114 id: Some(table_id.clone()),
6115 name: "old".to_string(),
6116 from_version: Some(1),
6117 ..Default::default()
6118 })
6119 .await
6120 .unwrap();
6121
6122 let old_ds = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
6123 .await
6124 .unwrap()
6125 .with_branch("old", None)
6126 .load()
6127 .await
6128 .unwrap();
6129 assert_eq!(old_ds.manifest.branch.as_deref(), Some("old"));
6130 assert_eq!(
6131 scan_id_column(&old_ds).await,
6132 vec![1],
6133 "the fork must contain only the fork-point data"
6134 );
6135 }
6136
6137 #[test]
6140 fn test_manifest_version_from_filename() {
6141 assert_eq!(
6143 DirectoryNamespace::manifest_version_from_filename("5.manifest"),
6144 Some(5)
6145 );
6146 assert_eq!(
6147 DirectoryNamespace::manifest_version_from_filename("0.manifest"),
6148 Some(0)
6149 );
6150 let v2_five = format!("{:020}.manifest", u64::MAX - 5);
6152 assert_eq!(
6153 DirectoryNamespace::manifest_version_from_filename(&v2_five),
6154 Some(5)
6155 );
6156 let v2_zero = format!("{:020}.manifest", u64::MAX);
6157 assert_eq!(
6158 DirectoryNamespace::manifest_version_from_filename(&v2_zero),
6159 Some(0)
6160 );
6161 assert_eq!(
6163 DirectoryNamespace::manifest_version_from_filename("data.lance"),
6164 None
6165 );
6166 assert_eq!(
6167 DirectoryNamespace::manifest_version_from_filename("d5.manifest"),
6168 None
6169 );
6170 }
6171
6172 #[tokio::test]
6173 async fn test_create_table() {
6174 let (namespace, _temp_dir) = create_test_namespace().await;
6175
6176 let schema = create_test_schema();
6178 let ipc_data = create_test_ipc_data(&schema);
6179
6180 let mut request = CreateTableRequest::new();
6181 request.id = Some(vec!["test_table".to_string()]);
6182
6183 let response = namespace
6184 .create_table(request, bytes::Bytes::from(ipc_data))
6185 .await
6186 .unwrap();
6187
6188 assert!(response.location.is_some());
6189 assert!(response.location.unwrap().ends_with("test_table.lance"));
6190 assert_eq!(response.version, Some(1));
6191 }
6192
6193 #[tokio::test]
6194 async fn test_create_table_without_data() {
6195 let (namespace, _temp_dir) = create_test_namespace().await;
6196
6197 let mut request = CreateTableRequest::new();
6198 request.id = Some(vec!["test_table".to_string()]);
6199
6200 let result = namespace.create_table(request, bytes::Bytes::new()).await;
6201 assert!(result.is_err());
6202 assert!(
6203 result
6204 .unwrap_err()
6205 .to_string()
6206 .contains("Arrow IPC stream) is required")
6207 );
6208 }
6209
6210 #[tokio::test]
6211 async fn test_create_table_with_invalid_id() {
6212 let (namespace, _temp_dir) = create_test_namespace().await;
6213
6214 let schema = create_test_schema();
6216 let ipc_data = create_test_ipc_data(&schema);
6217
6218 let mut request = CreateTableRequest::new();
6220 request.id = Some(vec![]);
6221
6222 let result = namespace
6223 .create_table(request, bytes::Bytes::from(ipc_data.clone()))
6224 .await;
6225 assert!(result.is_err());
6226
6227 let mut create_ns_req = CreateNamespaceRequest::new();
6230 create_ns_req.id = Some(vec!["test_namespace".to_string()]);
6231 namespace.create_namespace(create_ns_req).await.unwrap();
6232
6233 let mut request = CreateTableRequest::new();
6235 request.id = Some(vec!["test_namespace".to_string(), "table".to_string()]);
6236
6237 let result = namespace
6238 .create_table(request, bytes::Bytes::from(ipc_data))
6239 .await;
6240 assert!(
6242 result.is_ok(),
6243 "Multi-level table IDs should work with manifest enabled"
6244 );
6245 }
6246
6247 #[tokio::test]
6248 async fn test_list_tables() {
6249 let (namespace, _temp_dir) = create_test_namespace().await;
6250
6251 let mut request = ListTablesRequest::new();
6253 request.id = Some(vec![]);
6254 let response = namespace.list_tables(request).await.unwrap();
6255 assert_eq!(response.tables.len(), 0);
6256
6257 let schema = create_test_schema();
6259 let ipc_data = create_test_ipc_data(&schema);
6260
6261 let mut create_request = CreateTableRequest::new();
6263 create_request.id = Some(vec!["table1".to_string()]);
6264 namespace
6265 .create_table(create_request, bytes::Bytes::from(ipc_data.clone()))
6266 .await
6267 .unwrap();
6268
6269 let mut create_request = CreateTableRequest::new();
6271 create_request.id = Some(vec!["table2".to_string()]);
6272 namespace
6273 .create_table(create_request, bytes::Bytes::from(ipc_data))
6274 .await
6275 .unwrap();
6276
6277 let mut request = ListTablesRequest::new();
6279 request.id = Some(vec![]);
6280 let response = namespace.list_tables(request).await.unwrap();
6281 let tables = response.tables;
6282 assert_eq!(tables.len(), 2);
6283 assert!(tables.contains(&"table1".to_string()));
6284 assert!(tables.contains(&"table2".to_string()));
6285 }
6286
6287 #[tokio::test]
6288 async fn test_list_tables_pagination() {
6289 let (namespace, _temp_dir) = create_test_namespace().await;
6290
6291 let schema = create_test_schema();
6292 let ipc_data = create_test_ipc_data(&schema);
6293
6294 for name in ["alpha", "bravo", "charlie"] {
6295 let mut req = CreateTableRequest::new();
6296 req.id = Some(vec![name.to_string()]);
6297 namespace
6298 .create_table(req, bytes::Bytes::from(ipc_data.clone()))
6299 .await
6300 .unwrap();
6301 }
6302
6303 let first_page = namespace
6305 .list_tables(ListTablesRequest {
6306 id: Some(vec![]),
6307 limit: Some(2),
6308 ..Default::default()
6309 })
6310 .await
6311 .unwrap();
6312
6313 assert_eq!(first_page.tables, vec!["alpha", "bravo"]);
6314 assert_eq!(first_page.page_token.as_deref(), Some("bravo"));
6315
6316 let second_page = namespace
6318 .list_tables(ListTablesRequest {
6319 id: Some(vec![]),
6320 limit: Some(2),
6321 page_token: first_page.page_token.clone(),
6322 ..Default::default()
6323 })
6324 .await
6325 .unwrap();
6326
6327 assert_eq!(second_page.tables, vec!["charlie"]);
6328 assert!(second_page.page_token.is_none());
6329 }
6330
6331 #[tokio::test]
6332 async fn test_list_tables_pagination_limit_zero() {
6333 let (namespace, _temp_dir) = create_test_namespace().await;
6334
6335 let schema = create_test_schema();
6336 let ipc_data = create_test_ipc_data(&schema);
6337
6338 let mut req = CreateTableRequest::new();
6339 req.id = Some(vec!["alpha".to_string()]);
6340 namespace
6341 .create_table(req, bytes::Bytes::from(ipc_data))
6342 .await
6343 .unwrap();
6344
6345 let response = namespace
6346 .list_tables(ListTablesRequest {
6347 id: Some(vec![]),
6348 limit: Some(0),
6349 ..Default::default()
6350 })
6351 .await
6352 .unwrap();
6353
6354 assert!(response.tables.is_empty());
6355 assert!(response.page_token.is_none());
6356 }
6357
6358 #[tokio::test]
6359 async fn test_list_tables_with_namespace_id() {
6360 let (namespace, _temp_dir) = create_test_namespace().await;
6361
6362 let mut create_ns_req = CreateNamespaceRequest::new();
6364 create_ns_req.id = Some(vec!["test_namespace".to_string()]);
6365 namespace.create_namespace(create_ns_req).await.unwrap();
6366
6367 let mut request = ListTablesRequest::new();
6369 request.id = Some(vec!["test_namespace".to_string()]);
6370
6371 let result = namespace.list_tables(request).await;
6372 assert!(
6374 result.is_ok(),
6375 "list_tables should work with child namespace when manifest is enabled"
6376 );
6377 let response = result.unwrap();
6378 assert_eq!(
6379 response.tables.len(),
6380 0,
6381 "Namespace should have no tables yet"
6382 );
6383 }
6384
6385 #[tokio::test]
6386 async fn test_create_scalar_index() {
6387 let (namespace, _temp_dir) = create_test_namespace().await;
6388 create_scalar_table(&namespace, "users").await;
6389
6390 let transaction_id = create_scalar_index(&namespace, "users", "users_id_idx").await;
6391 let dataset = open_dataset(&namespace, "users").await;
6392 let expected_transaction_id = dataset
6393 .read_transaction()
6394 .await
6395 .unwrap()
6396 .map(|transaction| transaction.uuid);
6397 assert_eq!(transaction_id, expected_transaction_id);
6398 let indices = dataset.load_indices().await.unwrap();
6399 assert!(indices.iter().any(|index| index.name == "users_id_idx"));
6400 }
6401
6402 #[tokio::test]
6403 async fn test_create_vector_index() {
6404 use lance_namespace::models::CreateTableIndexRequest;
6405
6406 let (namespace, _temp_dir) = create_test_namespace().await;
6407 create_vector_table(&namespace, "vectors").await;
6408
6409 let mut create_index_request =
6410 CreateTableIndexRequest::new("vector".to_string(), "IVF_FLAT".to_string());
6411 create_index_request.id = Some(vec!["vectors".to_string()]);
6412 create_index_request.name = Some("vector_idx".to_string());
6413 create_index_request.distance_type = Some("l2".to_string());
6414 let transaction_id = namespace
6415 .create_table_index(create_index_request)
6416 .await
6417 .unwrap()
6418 .transaction_id;
6419
6420 let dataset = open_dataset(&namespace, "vectors").await;
6421 let expected_transaction_id = dataset
6422 .read_transaction()
6423 .await
6424 .unwrap()
6425 .map(|transaction| transaction.uuid);
6426 assert_eq!(transaction_id, expected_transaction_id);
6427 let indices = dataset.load_indices().await.unwrap();
6428 assert!(indices.iter().any(|index| index.name == "vector_idx"));
6429 }
6430
6431 #[tokio::test]
6432 async fn test_list_table_indices() {
6433 use lance_namespace::models::{CreateTableIndexRequest, ListTableIndicesRequest};
6434
6435 let (namespace, _temp_dir) = create_test_namespace().await;
6436 create_scalar_table(&namespace, "users").await;
6437 create_scalar_index(&namespace, "users", "a_idx").await;
6438 create_scalar_index(&namespace, "users", "b_idx").await;
6439 let transaction_id = create_scalar_index(&namespace, "users", "users_id_idx").await;
6440
6441 let response = namespace
6442 .list_table_indices(ListTableIndicesRequest {
6443 id: Some(vec!["users".to_string()]),
6444 ..Default::default()
6445 })
6446 .await
6447 .unwrap();
6448
6449 assert_eq!(response.indexes.len(), 3);
6450 assert_eq!(response.indexes[0].index_name, "a_idx");
6451 assert_eq!(response.indexes[1].index_name, "b_idx");
6452 assert_eq!(response.indexes[2].index_name, "users_id_idx");
6453 assert!(response.page_token.is_none());
6454 let users_id_idx = response
6455 .indexes
6456 .iter()
6457 .find(|index| index.index_name == "users_id_idx")
6458 .unwrap();
6459 assert_eq!(users_id_idx.columns, vec!["id"]);
6460 assert_eq!(users_id_idx.status, "SUCCEEDED");
6461
6462 assert_eq!(users_id_idx.index_type.as_deref(), Some("BTree"));
6464 assert!(
6465 users_id_idx
6466 .type_url
6467 .as_deref()
6468 .is_some_and(|s| !s.is_empty())
6469 );
6470 assert_eq!(users_id_idx.num_indexed_rows, Some(3));
6471 assert_eq!(users_id_idx.num_unindexed_rows, Some(0));
6472 assert_eq!(users_id_idx.num_segments, Some(1));
6473 assert!(users_id_idx.size_bytes.is_some_and(|size| size > 0));
6474 assert!(users_id_idx.created_at.is_some());
6475 assert!(users_id_idx.index_version.is_some());
6476 assert!(users_id_idx.index_details.is_some());
6477
6478 let dataset = open_dataset(&namespace, "users").await;
6479 let expected_transaction_id = dataset
6480 .read_transaction()
6481 .await
6482 .unwrap()
6483 .map(|transaction| transaction.uuid);
6484 assert_eq!(transaction_id, expected_transaction_id);
6485 let indices = dataset.load_indices().await.unwrap();
6486 assert_eq!(
6487 indices
6488 .iter()
6489 .filter(|index| index.name == "users_id_idx")
6490 .count(),
6491 1
6492 );
6493
6494 let first_page = namespace
6495 .list_table_indices(ListTableIndicesRequest {
6496 id: Some(vec!["users".to_string()]),
6497 limit: Some(2),
6498 ..Default::default()
6499 })
6500 .await
6501 .unwrap();
6502
6503 assert_eq!(first_page.indexes.len(), 2);
6504 assert_eq!(first_page.indexes[0].index_name, "a_idx");
6505 assert_eq!(first_page.indexes[1].index_name, "b_idx");
6506 assert_eq!(first_page.page_token.as_deref(), Some("b_idx"));
6507
6508 let second_page = namespace
6509 .list_table_indices(ListTableIndicesRequest {
6510 id: Some(vec!["users".to_string()]),
6511 page_token: first_page.page_token.clone(),
6512 limit: Some(2),
6513 ..Default::default()
6514 })
6515 .await
6516 .unwrap();
6517
6518 assert_eq!(second_page.indexes.len(), 1);
6519 assert_eq!(second_page.indexes[0].index_name, "users_id_idx");
6520 assert!(second_page.page_token.is_none());
6521
6522 create_vector_table(&namespace, "vectors").await;
6524 let mut create_index_request =
6525 CreateTableIndexRequest::new("vector".to_string(), "IVF_FLAT".to_string());
6526 create_index_request.id = Some(vec!["vectors".to_string()]);
6527 create_index_request.name = Some("vector_idx".to_string());
6528 create_index_request.distance_type = Some("l2".to_string());
6529 namespace
6530 .create_table_index(create_index_request)
6531 .await
6532 .unwrap();
6533
6534 let vector_response = namespace
6535 .list_table_indices(ListTableIndicesRequest {
6536 id: Some(vec!["vectors".to_string()]),
6537 ..Default::default()
6538 })
6539 .await
6540 .unwrap();
6541
6542 assert_eq!(vector_response.indexes.len(), 1);
6543 let vector_idx = &vector_response.indexes[0];
6544 assert_eq!(vector_idx.index_name, "vector_idx");
6545 assert_eq!(vector_idx.columns, vec!["vector"]);
6546 assert_eq!(vector_idx.index_type.as_deref(), Some("IVF_FLAT"));
6547 assert!(
6548 vector_idx
6549 .type_url
6550 .as_deref()
6551 .is_some_and(|s| !s.is_empty())
6552 );
6553 assert!(vector_idx.num_indexed_rows.is_some());
6554 assert!(vector_idx.num_unindexed_rows.is_some());
6555 assert_eq!(vector_idx.num_segments, Some(1));
6556 assert!(vector_idx.created_at.is_some());
6557 assert!(vector_idx.index_version.is_some());
6558 assert!(vector_idx.index_details.is_some());
6559 }
6560
6561 #[tokio::test]
6562 async fn test_describe_table_index_stats() {
6563 use lance_namespace::models::DescribeTableIndexStatsRequest;
6564
6565 let (namespace, _temp_dir) = create_test_namespace().await;
6566 create_scalar_table(&namespace, "users").await;
6567 let transaction_id = create_scalar_index(&namespace, "users", "users_id_idx").await;
6568
6569 let response = namespace
6570 .describe_table_index_stats(DescribeTableIndexStatsRequest {
6571 id: Some(vec!["users".to_string()]),
6572 index_name: Some("users_id_idx".to_string()),
6573 ..Default::default()
6574 })
6575 .await
6576 .unwrap();
6577 assert_eq!(response.index_type, Some("BTree".to_string()));
6578 assert_eq!(response.num_indices, Some(1));
6579 assert_eq!(response.num_indexed_rows, Some(3));
6580 assert_eq!(response.num_unindexed_rows, Some(0));
6581
6582 let dataset = open_dataset(&namespace, "users").await;
6583 let expected_transaction_id = dataset
6584 .read_transaction()
6585 .await
6586 .unwrap()
6587 .map(|transaction| transaction.uuid);
6588 assert_eq!(transaction_id, expected_transaction_id);
6589 let stats: serde_json::Value =
6590 serde_json::from_str(&dataset.index_statistics("users_id_idx").await.unwrap()).unwrap();
6591 assert_eq!(stats["index_type"], "BTree");
6592 assert_eq!(stats["num_indices"], 1);
6593 assert_eq!(stats["num_indexed_rows"], 3);
6594 assert_eq!(stats["num_unindexed_rows"], 0);
6595 }
6596
6597 #[tokio::test]
6598 async fn test_describe_transaction() {
6599 use lance_namespace::models::DescribeTransactionRequest;
6600
6601 let (namespace, _temp_dir) = create_test_namespace().await;
6602 create_scalar_table(&namespace, "users").await;
6603 let transaction_id = create_scalar_index(&namespace, "users", "users_id_idx").await;
6604 let dataset = open_dataset(&namespace, "users").await;
6605 let latest_transaction = dataset.read_transaction().await.unwrap();
6606 assert_eq!(
6607 transaction_id,
6608 latest_transaction
6609 .as_ref()
6610 .map(|transaction| transaction.uuid.clone())
6611 );
6612
6613 if let Some(transaction_id) = transaction_id {
6614 let response = namespace
6615 .describe_transaction(DescribeTransactionRequest {
6616 id: Some(vec!["users".to_string(), transaction_id.clone()]),
6617 ..Default::default()
6618 })
6619 .await
6620 .unwrap();
6621 assert_eq!(response.status, "SUCCEEDED");
6622 assert_eq!(
6623 response
6624 .properties
6625 .as_ref()
6626 .and_then(|props| props.get("operation")),
6627 Some(&"CreateIndex".to_string())
6628 );
6629 assert_eq!(
6630 response
6631 .properties
6632 .as_ref()
6633 .and_then(|props| props.get("uuid")),
6634 Some(&transaction_id)
6635 );
6636 } else {
6637 assert!(latest_transaction.is_none());
6638 }
6639 }
6640
6641 #[tokio::test]
6642 async fn test_drop_table_index() {
6643 use lance_namespace::models::{DropTableIndexRequest, ListTableIndicesRequest};
6644
6645 let (namespace, _temp_dir) = create_test_namespace().await;
6646 create_scalar_table(&namespace, "users").await;
6647 let create_transaction_id = create_scalar_index(&namespace, "users", "users_id_idx").await;
6648
6649 let drop_transaction_id = namespace
6650 .drop_table_index(DropTableIndexRequest {
6651 id: Some(vec!["users".to_string()]),
6652 index_name: Some("users_id_idx".to_string()),
6653 ..Default::default()
6654 })
6655 .await
6656 .unwrap()
6657 .transaction_id;
6658
6659 let dataset = open_dataset(&namespace, "users").await;
6660 let previous_dataset = dataset
6661 .checkout_version(dataset.version().version - 1)
6662 .await
6663 .unwrap();
6664 let previous_transaction_id = previous_dataset
6665 .read_transaction()
6666 .await
6667 .unwrap()
6668 .map(|transaction| transaction.uuid);
6669 assert_eq!(create_transaction_id, previous_transaction_id);
6670 let expected_drop_transaction_id = dataset
6671 .read_transaction()
6672 .await
6673 .unwrap()
6674 .map(|transaction| transaction.uuid);
6675 assert_eq!(drop_transaction_id, expected_drop_transaction_id);
6676 let indices = dataset.load_indices().await.unwrap();
6677 assert!(!indices.iter().any(|index| index.name == "users_id_idx"));
6678
6679 let list_response = namespace
6680 .list_table_indices(ListTableIndicesRequest {
6681 id: Some(vec!["users".to_string()]),
6682 ..Default::default()
6683 })
6684 .await
6685 .unwrap();
6686 assert!(list_response.indexes.is_empty());
6687 }
6688
6689 #[tokio::test]
6690 async fn test_describe_table() {
6691 let (namespace, _temp_dir) = create_test_namespace().await;
6692
6693 let schema = create_test_schema();
6695 let ipc_data = create_test_ipc_data(&schema);
6696
6697 let mut create_request = CreateTableRequest::new();
6698 create_request.id = Some(vec!["test_table".to_string()]);
6699 namespace
6700 .create_table(create_request, bytes::Bytes::from(ipc_data))
6701 .await
6702 .unwrap();
6703
6704 let mut request = DescribeTableRequest::new();
6706 request.id = Some(vec!["test_table".to_string()]);
6707 let response = namespace.describe_table(request).await.unwrap();
6708
6709 assert!(response.location.is_some());
6710 assert!(response.location.unwrap().ends_with("test_table.lance"));
6711 }
6712
6713 #[tokio::test]
6714 async fn test_describe_nonexistent_table() {
6715 let (namespace, _temp_dir) = create_test_namespace().await;
6716
6717 let mut request = DescribeTableRequest::new();
6718 request.id = Some(vec!["nonexistent".to_string()]);
6719
6720 let result = namespace.describe_table(request).await;
6721 assert!(result.is_err());
6722 assert!(result.unwrap_err().to_string().contains("Table not found"));
6723 }
6724
6725 #[tokio::test]
6726 async fn test_table_exists() {
6727 let (namespace, _temp_dir) = create_test_namespace().await;
6728
6729 let schema = create_test_schema();
6731 let ipc_data = create_test_ipc_data(&schema);
6732
6733 let mut create_request = CreateTableRequest::new();
6734 create_request.id = Some(vec!["existing_table".to_string()]);
6735 namespace
6736 .create_table(create_request, bytes::Bytes::from(ipc_data))
6737 .await
6738 .unwrap();
6739
6740 let mut request = TableExistsRequest::new();
6742 request.id = Some(vec!["existing_table".to_string()]);
6743 let result = namespace.table_exists(request).await;
6744 assert!(result.is_ok());
6745
6746 let mut request = TableExistsRequest::new();
6748 request.id = Some(vec!["nonexistent".to_string()]);
6749 let result = namespace.table_exists(request).await;
6750 assert!(result.is_err());
6751 assert!(result.unwrap_err().to_string().contains("Table not found"));
6752 }
6753
6754 #[tokio::test]
6755 async fn test_drop_table() {
6756 let (namespace, _temp_dir) = create_test_namespace().await;
6757
6758 let schema = create_test_schema();
6760 let ipc_data = create_test_ipc_data(&schema);
6761
6762 let mut create_request = CreateTableRequest::new();
6763 create_request.id = Some(vec!["table_to_drop".to_string()]);
6764 namespace
6765 .create_table(create_request, bytes::Bytes::from(ipc_data))
6766 .await
6767 .unwrap();
6768
6769 let mut exists_request = TableExistsRequest::new();
6771 exists_request.id = Some(vec!["table_to_drop".to_string()]);
6772 assert!(namespace.table_exists(exists_request.clone()).await.is_ok());
6773
6774 let mut drop_request = DropTableRequest::new();
6776 drop_request.id = Some(vec!["table_to_drop".to_string()]);
6777 let response = namespace.drop_table(drop_request).await.unwrap();
6778 assert!(response.location.is_some());
6779
6780 assert!(namespace.table_exists(exists_request).await.is_err());
6782 }
6783
6784 #[tokio::test]
6785 async fn test_drop_nonexistent_table() {
6786 let (namespace, _temp_dir) = create_test_namespace().await;
6787
6788 let mut request = DropTableRequest::new();
6789 request.id = Some(vec!["nonexistent".to_string()]);
6790
6791 let result = namespace.drop_table(request).await;
6793 let _ = result;
6796 }
6797
6798 #[tokio::test]
6799 async fn test_root_namespace_operations() {
6800 let (namespace, _temp_dir) = create_test_namespace().await;
6801
6802 let mut request = ListNamespacesRequest::new();
6804 request.id = Some(vec![]);
6805 let result = namespace.list_namespaces(request).await;
6806 assert!(result.is_ok());
6807 assert_eq!(result.unwrap().namespaces.len(), 0);
6808
6809 let mut request = DescribeNamespaceRequest::new();
6811 request.id = Some(vec![]);
6812 let result = namespace.describe_namespace(request).await;
6813 assert!(result.is_ok());
6814
6815 let mut request = NamespaceExistsRequest::new();
6817 request.id = Some(vec![]);
6818 let result = namespace.namespace_exists(request).await;
6819 assert!(result.is_ok());
6820
6821 let mut request = CreateNamespaceRequest::new();
6823 request.id = Some(vec![]);
6824 let result = namespace.create_namespace(request).await;
6825 assert!(result.is_err());
6826 assert!(result.unwrap_err().to_string().contains("already exists"));
6827
6828 let mut request = DropNamespaceRequest::new();
6830 request.id = Some(vec![]);
6831 let result = namespace.drop_namespace(request).await;
6832 assert!(result.is_err());
6833 assert!(
6834 result
6835 .unwrap_err()
6836 .to_string()
6837 .contains("cannot be dropped")
6838 );
6839 }
6840
6841 #[tokio::test]
6842 async fn test_non_root_namespace_operations() {
6843 let (namespace, _temp_dir) = create_test_namespace().await;
6844
6845 let mut request = CreateNamespaceRequest::new();
6848 request.id = Some(vec!["child".to_string()]);
6849 let result = namespace.create_namespace(request).await;
6850 assert!(
6851 result.is_ok(),
6852 "Child namespace creation should succeed with manifest enabled"
6853 );
6854
6855 let mut request = NamespaceExistsRequest::new();
6857 request.id = Some(vec!["child".to_string()]);
6858 let result = namespace.namespace_exists(request).await;
6859 assert!(
6860 result.is_ok(),
6861 "Child namespace should exist after creation"
6862 );
6863
6864 let mut request = DropNamespaceRequest::new();
6866 request.id = Some(vec!["child".to_string()]);
6867 let result = namespace.drop_namespace(request).await;
6868 assert!(
6869 result.is_ok(),
6870 "Child namespace drop should succeed with manifest enabled"
6871 );
6872
6873 let mut request = NamespaceExistsRequest::new();
6875 request.id = Some(vec!["child".to_string()]);
6876 let result = namespace.namespace_exists(request).await;
6877 assert!(
6878 result.is_err(),
6879 "Child namespace should not exist after drop"
6880 );
6881 }
6882
6883 #[tokio::test]
6884 async fn test_config_custom_root() {
6885 let temp_dir = TempStdDir::default();
6886 let custom_path = temp_dir.join("custom");
6887 std::fs::create_dir(&custom_path).unwrap();
6888
6889 let namespace = DirectoryNamespaceBuilder::new(custom_path.to_string_lossy().to_string())
6890 .build()
6891 .await
6892 .unwrap();
6893
6894 let schema = create_test_schema();
6896 let ipc_data = create_test_ipc_data(&schema);
6897
6898 let mut request = CreateTableRequest::new();
6900 request.id = Some(vec!["test_table".to_string()]);
6901
6902 let response = namespace
6903 .create_table(request, bytes::Bytes::from(ipc_data))
6904 .await
6905 .unwrap();
6906
6907 assert!(response.location.unwrap().contains("custom"));
6908 }
6909
6910 #[tokio::test]
6911 async fn test_config_storage_options() {
6912 let temp_dir = TempStdDir::default();
6913
6914 let namespace = DirectoryNamespaceBuilder::new(temp_dir.to_str().unwrap())
6915 .storage_option("option1", "value1")
6916 .storage_option("option2", "value2")
6917 .build()
6918 .await
6919 .unwrap();
6920
6921 let schema = create_test_schema();
6923 let ipc_data = create_test_ipc_data(&schema);
6924
6925 let mut request = CreateTableRequest::new();
6927 request.id = Some(vec!["test_table".to_string()]);
6928
6929 let response = namespace
6930 .create_table(request, bytes::Bytes::from(ipc_data))
6931 .await
6932 .unwrap();
6933
6934 let storage_options = response.storage_options.unwrap();
6935 assert_eq!(storage_options.get("option1"), Some(&"value1".to_string()));
6936 assert_eq!(storage_options.get("option2"), Some(&"value2".to_string()));
6937 }
6938
6939 #[tokio::test]
6943 async fn test_no_storage_options_without_vendor() {
6944 use lance_namespace::models::DeclareTableRequest;
6945
6946 let temp_dir = TempStdDir::default();
6947
6948 let namespace = DirectoryNamespaceBuilder::new(temp_dir.to_str().unwrap())
6950 .manifest_enabled(false)
6951 .storage_option("aws_access_key_id", "AKID")
6952 .storage_option("aws_secret_access_key", "SECRET")
6953 .storage_option("region", "us-east-1")
6954 .build()
6955 .await
6956 .unwrap();
6957
6958 let schema = create_test_schema();
6959 let ipc_data = create_test_ipc_data(&schema);
6960
6961 let mut create_req = CreateTableRequest::new();
6963 create_req.id = Some(vec!["t1".to_string()]);
6964 namespace
6965 .create_table(create_req, bytes::Bytes::from(ipc_data))
6966 .await
6967 .unwrap();
6968
6969 let mut desc_req = DescribeTableRequest::new();
6971 desc_req.id = Some(vec!["t1".to_string()]);
6972 let resp = namespace.describe_table(desc_req).await.unwrap();
6973 assert!(resp.storage_options.is_none());
6974
6975 let mut decl_req = DeclareTableRequest::new();
6977 decl_req.id = Some(vec!["t2".to_string()]);
6978 let resp = namespace.declare_table(decl_req).await.unwrap();
6979 assert!(resp.storage_options.is_none());
6980 }
6981
6982 #[tokio::test]
6984 async fn test_no_storage_options_without_vendor_manifest() {
6985 let temp_dir = TempStdDir::default();
6986
6987 let namespace = DirectoryNamespaceBuilder::new(temp_dir.to_str().unwrap())
6988 .storage_option("aws_access_key_id", "AKID")
6989 .storage_option("aws_secret_access_key", "SECRET")
6990 .storage_option("region", "us-east-1")
6991 .build()
6992 .await
6993 .unwrap();
6994
6995 let schema = create_test_schema();
6996 let ipc_data = create_test_ipc_data(&schema);
6997
6998 let mut create_req = CreateTableRequest::new();
6999 create_req.id = Some(vec!["t1".to_string()]);
7000 namespace
7001 .create_table(create_req, bytes::Bytes::from(ipc_data))
7002 .await
7003 .unwrap();
7004
7005 let mut desc_req = DescribeTableRequest::new();
7007 desc_req.id = Some(vec!["t1".to_string()]);
7008 let resp = namespace.describe_table(desc_req).await.unwrap();
7009 assert!(resp.storage_options.is_none());
7010 }
7011
7012 #[tokio::test]
7013 async fn test_from_properties_manifest_enabled() {
7014 let temp_dir = TempStdDir::default();
7015
7016 let mut properties = HashMap::new();
7017 properties.insert("root".to_string(), temp_dir.to_str().unwrap().to_string());
7018 properties.insert("manifest_enabled".to_string(), "true".to_string());
7019 properties.insert("dir_listing_enabled".to_string(), "false".to_string());
7020
7021 let builder = DirectoryNamespaceBuilder::from_properties(properties, None).unwrap();
7022 assert!(builder.manifest_enabled);
7023 assert!(!builder.dir_listing_enabled);
7024
7025 let namespace = builder.build().await.unwrap();
7026
7027 let schema = create_test_schema();
7029 let ipc_data = create_test_ipc_data(&schema);
7030
7031 let mut request = CreateTableRequest::new();
7033 request.id = Some(vec!["test_table".to_string()]);
7034
7035 let response = namespace
7036 .create_table(request, bytes::Bytes::from(ipc_data))
7037 .await
7038 .unwrap();
7039
7040 assert!(response.location.is_some());
7041 }
7042
7043 #[tokio::test]
7044 async fn test_from_properties_dir_listing_enabled() {
7045 let temp_dir = TempStdDir::default();
7046
7047 let mut properties = HashMap::new();
7048 properties.insert("root".to_string(), temp_dir.to_str().unwrap().to_string());
7049 properties.insert("manifest_enabled".to_string(), "false".to_string());
7050 properties.insert("dir_listing_enabled".to_string(), "true".to_string());
7051
7052 let builder = DirectoryNamespaceBuilder::from_properties(properties, None).unwrap();
7053 assert!(!builder.manifest_enabled);
7054 assert!(builder.dir_listing_enabled);
7055
7056 let namespace = builder.build().await.unwrap();
7057
7058 let schema = create_test_schema();
7060 let ipc_data = create_test_ipc_data(&schema);
7061
7062 let mut request = CreateTableRequest::new();
7064 request.id = Some(vec!["test_table".to_string()]);
7065
7066 let response = namespace
7067 .create_table(request, bytes::Bytes::from(ipc_data))
7068 .await
7069 .unwrap();
7070
7071 assert!(response.location.is_some());
7072 }
7073
7074 #[tokio::test]
7075 async fn test_from_properties_defaults() {
7076 let temp_dir = TempStdDir::default();
7077
7078 let mut properties = HashMap::new();
7079 properties.insert("root".to_string(), temp_dir.to_str().unwrap().to_string());
7080
7081 let builder = DirectoryNamespaceBuilder::from_properties(properties, None).unwrap();
7082 assert!(builder.manifest_enabled);
7084 assert!(builder.dir_listing_enabled);
7085 }
7086
7087 #[tokio::test]
7088 async fn test_from_properties_with_storage_options() {
7089 let temp_dir = TempStdDir::default();
7090
7091 let mut properties = HashMap::new();
7092 properties.insert("root".to_string(), temp_dir.to_str().unwrap().to_string());
7093 properties.insert("manifest_enabled".to_string(), "true".to_string());
7094 properties.insert("storage.region".to_string(), "us-west-2".to_string());
7095 properties.insert("storage.bucket".to_string(), "my-bucket".to_string());
7096
7097 let builder = DirectoryNamespaceBuilder::from_properties(properties, None).unwrap();
7098 assert!(builder.manifest_enabled);
7099 assert!(builder.storage_options.is_some());
7100
7101 let storage_options = builder.storage_options.unwrap();
7102 assert_eq!(
7103 storage_options.get("region"),
7104 Some(&"us-west-2".to_string())
7105 );
7106 assert_eq!(
7107 storage_options.get("bucket"),
7108 Some(&"my-bucket".to_string())
7109 );
7110 }
7111
7112 #[tokio::test]
7113 async fn test_various_arrow_types() {
7114 let (namespace, _temp_dir) = create_test_namespace().await;
7115
7116 let fields = vec![
7118 JsonArrowField {
7119 name: "bool_col".to_string(),
7120 r#type: Box::new(JsonArrowDataType::new("bool".to_string())),
7121 nullable: true,
7122 metadata: None,
7123 },
7124 JsonArrowField {
7125 name: "int8_col".to_string(),
7126 r#type: Box::new(JsonArrowDataType::new("int8".to_string())),
7127 nullable: true,
7128 metadata: None,
7129 },
7130 JsonArrowField {
7131 name: "float64_col".to_string(),
7132 r#type: Box::new(JsonArrowDataType::new("float64".to_string())),
7133 nullable: true,
7134 metadata: None,
7135 },
7136 JsonArrowField {
7137 name: "binary_col".to_string(),
7138 r#type: Box::new(JsonArrowDataType::new("binary".to_string())),
7139 nullable: true,
7140 metadata: None,
7141 },
7142 ];
7143
7144 let schema = JsonArrowSchema {
7145 fields,
7146 metadata: None,
7147 };
7148
7149 let ipc_data = create_test_ipc_data(&schema);
7151
7152 let mut request = CreateTableRequest::new();
7153 request.id = Some(vec!["complex_table".to_string()]);
7154
7155 let response = namespace
7156 .create_table(request, bytes::Bytes::from(ipc_data))
7157 .await
7158 .unwrap();
7159
7160 assert!(response.location.is_some());
7161 }
7162
7163 #[tokio::test]
7164 async fn test_connect_dir() {
7165 let temp_dir = TempStdDir::default();
7166
7167 let namespace = DirectoryNamespaceBuilder::new(temp_dir.to_str().unwrap())
7168 .build()
7169 .await
7170 .unwrap();
7171
7172 let mut request = ListTablesRequest::new();
7174 request.id = Some(vec![]);
7175 let response = namespace.list_tables(request).await.unwrap();
7176 assert_eq!(response.tables.len(), 0);
7177 }
7178
7179 #[tokio::test]
7180 async fn test_create_table_with_ipc_data() {
7181 use arrow::array::{Int32Array, StringArray};
7182 use arrow::ipc::writer::StreamWriter;
7183
7184 let (namespace, _temp_dir) = create_test_namespace().await;
7185
7186 let schema = create_test_schema();
7188
7189 let arrow_schema = convert_json_arrow_schema(&schema).unwrap();
7191 let arrow_schema = Arc::new(arrow_schema);
7192
7193 let id_array = Int32Array::from(vec![1, 2, 3]);
7195 let name_array = StringArray::from(vec!["Alice", "Bob", "Charlie"]);
7196 let batch = arrow::record_batch::RecordBatch::try_new(
7197 arrow_schema.clone(),
7198 vec![Arc::new(id_array), Arc::new(name_array)],
7199 )
7200 .unwrap();
7201
7202 let mut buffer = Vec::new();
7204 {
7205 let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
7206 writer.write(&batch).unwrap();
7207 writer.finish().unwrap();
7208 }
7209
7210 let mut request = CreateTableRequest::new();
7212 request.id = Some(vec!["test_table_with_data".to_string()]);
7213
7214 let response = namespace
7215 .create_table(request, Bytes::from(buffer))
7216 .await
7217 .unwrap();
7218
7219 assert_eq!(response.version, Some(1));
7220 assert!(
7221 response
7222 .location
7223 .unwrap()
7224 .contains("test_table_with_data.lance")
7225 );
7226
7227 let mut exists_request = TableExistsRequest::new();
7229 exists_request.id = Some(vec!["test_table_with_data".to_string()]);
7230 namespace.table_exists(exists_request).await.unwrap();
7231 }
7232
7233 #[tokio::test]
7234 async fn test_child_namespace_create_and_list() {
7235 let (namespace, _temp_dir) = create_test_namespace().await;
7236
7237 for i in 1..=3 {
7239 let mut create_req = CreateNamespaceRequest::new();
7240 create_req.id = Some(vec![format!("ns{}", i)]);
7241 let result = namespace.create_namespace(create_req).await;
7242 assert!(result.is_ok(), "Failed to create child namespace ns{}", i);
7243 }
7244
7245 let list_req = ListNamespacesRequest {
7247 id: Some(vec![]),
7248 ..Default::default()
7249 };
7250 let result = namespace.list_namespaces(list_req).await;
7251 assert!(result.is_ok());
7252 let namespaces = result.unwrap().namespaces;
7253 assert_eq!(namespaces.len(), 3);
7254 assert!(namespaces.contains(&"ns1".to_string()));
7255 assert!(namespaces.contains(&"ns2".to_string()));
7256 assert!(namespaces.contains(&"ns3".to_string()));
7257 }
7258
7259 #[tokio::test]
7260 async fn test_nested_namespace_hierarchy() {
7261 let (namespace, _temp_dir) = create_test_namespace().await;
7262
7263 let mut create_req = CreateNamespaceRequest::new();
7265 create_req.id = Some(vec!["parent".to_string()]);
7266 namespace.create_namespace(create_req).await.unwrap();
7267
7268 let mut create_req = CreateNamespaceRequest::new();
7270 create_req.id = Some(vec!["parent".to_string(), "child1".to_string()]);
7271 namespace.create_namespace(create_req).await.unwrap();
7272
7273 let mut create_req = CreateNamespaceRequest::new();
7274 create_req.id = Some(vec!["parent".to_string(), "child2".to_string()]);
7275 namespace.create_namespace(create_req).await.unwrap();
7276
7277 let list_req = ListNamespacesRequest {
7279 id: Some(vec!["parent".to_string()]),
7280 ..Default::default()
7281 };
7282 let result = namespace.list_namespaces(list_req).await;
7283 assert!(result.is_ok());
7284 let children = result.unwrap().namespaces;
7285 assert_eq!(children.len(), 2);
7286 assert!(children.contains(&"child1".to_string()));
7287 assert!(children.contains(&"child2".to_string()));
7288
7289 let list_req = ListNamespacesRequest {
7291 id: Some(vec![]),
7292 ..Default::default()
7293 };
7294 let result = namespace.list_namespaces(list_req).await;
7295 assert!(result.is_ok());
7296 let root_namespaces = result.unwrap().namespaces;
7297 assert_eq!(root_namespaces.len(), 1);
7298 assert_eq!(root_namespaces[0], "parent");
7299 }
7300
7301 #[tokio::test]
7302 async fn test_table_in_child_namespace() {
7303 let (namespace, _temp_dir) = create_test_namespace().await;
7304
7305 let mut create_ns_req = CreateNamespaceRequest::new();
7307 create_ns_req.id = Some(vec!["test_ns".to_string()]);
7308 namespace.create_namespace(create_ns_req).await.unwrap();
7309
7310 let schema = create_test_schema();
7312 let ipc_data = create_test_ipc_data(&schema);
7313 let mut create_table_req = CreateTableRequest::new();
7314 create_table_req.id = Some(vec!["test_ns".to_string(), "table1".to_string()]);
7315 let result = namespace
7316 .create_table(create_table_req, bytes::Bytes::from(ipc_data))
7317 .await;
7318 assert!(result.is_ok(), "Failed to create table in child namespace");
7319
7320 let list_req = ListTablesRequest {
7322 id: Some(vec!["test_ns".to_string()]),
7323 ..Default::default()
7324 };
7325 let result = namespace.list_tables(list_req).await;
7326 assert!(result.is_ok());
7327 let tables = result.unwrap().tables;
7328 assert_eq!(tables.len(), 1);
7329 assert_eq!(tables[0], "table1");
7330
7331 let mut exists_req = TableExistsRequest::new();
7333 exists_req.id = Some(vec!["test_ns".to_string(), "table1".to_string()]);
7334 let result = namespace.table_exists(exists_req).await;
7335 assert!(result.is_ok());
7336
7337 let mut describe_req = DescribeTableRequest::new();
7339 describe_req.id = Some(vec!["test_ns".to_string(), "table1".to_string()]);
7340 let result = namespace.describe_table(describe_req).await;
7341 assert!(result.is_ok());
7342 let response = result.unwrap();
7343 assert!(response.location.is_some());
7344 }
7345
7346 #[tokio::test]
7347 async fn test_multiple_tables_in_child_namespace() {
7348 let (namespace, _temp_dir) = create_test_namespace().await;
7349
7350 let mut create_ns_req = CreateNamespaceRequest::new();
7352 create_ns_req.id = Some(vec!["test_ns".to_string()]);
7353 namespace.create_namespace(create_ns_req).await.unwrap();
7354
7355 let schema = create_test_schema();
7357 let ipc_data = create_test_ipc_data(&schema);
7358 for i in 1..=3 {
7359 let mut create_table_req = CreateTableRequest::new();
7360 create_table_req.id = Some(vec!["test_ns".to_string(), format!("table{}", i)]);
7361 namespace
7362 .create_table(create_table_req, bytes::Bytes::from(ipc_data.clone()))
7363 .await
7364 .unwrap();
7365 }
7366
7367 let list_req = ListTablesRequest {
7369 id: Some(vec!["test_ns".to_string()]),
7370 ..Default::default()
7371 };
7372 let result = namespace.list_tables(list_req).await;
7373 assert!(result.is_ok());
7374 let tables = result.unwrap().tables;
7375 assert_eq!(tables.len(), 3);
7376 assert!(tables.contains(&"table1".to_string()));
7377 assert!(tables.contains(&"table2".to_string()));
7378 assert!(tables.contains(&"table3".to_string()));
7379 }
7380
7381 #[tokio::test]
7382 async fn test_drop_table_in_child_namespace() {
7383 let (namespace, _temp_dir) = create_test_namespace().await;
7384
7385 let mut create_ns_req = CreateNamespaceRequest::new();
7387 create_ns_req.id = Some(vec!["test_ns".to_string()]);
7388 namespace.create_namespace(create_ns_req).await.unwrap();
7389
7390 let schema = create_test_schema();
7392 let ipc_data = create_test_ipc_data(&schema);
7393 let mut create_table_req = CreateTableRequest::new();
7394 create_table_req.id = Some(vec!["test_ns".to_string(), "table1".to_string()]);
7395 namespace
7396 .create_table(create_table_req, bytes::Bytes::from(ipc_data))
7397 .await
7398 .unwrap();
7399
7400 let mut drop_req = DropTableRequest::new();
7402 drop_req.id = Some(vec!["test_ns".to_string(), "table1".to_string()]);
7403 let result = namespace.drop_table(drop_req).await;
7404 assert!(result.is_ok(), "Failed to drop table in child namespace");
7405
7406 let mut exists_req = TableExistsRequest::new();
7408 exists_req.id = Some(vec!["test_ns".to_string(), "table1".to_string()]);
7409 let result = namespace.table_exists(exists_req).await;
7410 assert!(result.is_err());
7411 }
7412
7413 #[tokio::test]
7414 async fn test_deeply_nested_namespace() {
7415 let (namespace, _temp_dir) = create_test_namespace().await;
7416
7417 let mut create_req = CreateNamespaceRequest::new();
7419 create_req.id = Some(vec!["level1".to_string()]);
7420 namespace.create_namespace(create_req).await.unwrap();
7421
7422 let mut create_req = CreateNamespaceRequest::new();
7423 create_req.id = Some(vec!["level1".to_string(), "level2".to_string()]);
7424 namespace.create_namespace(create_req).await.unwrap();
7425
7426 let mut create_req = CreateNamespaceRequest::new();
7427 create_req.id = Some(vec![
7428 "level1".to_string(),
7429 "level2".to_string(),
7430 "level3".to_string(),
7431 ]);
7432 namespace.create_namespace(create_req).await.unwrap();
7433
7434 let schema = create_test_schema();
7436 let ipc_data = create_test_ipc_data(&schema);
7437 let mut create_table_req = CreateTableRequest::new();
7438 create_table_req.id = Some(vec![
7439 "level1".to_string(),
7440 "level2".to_string(),
7441 "level3".to_string(),
7442 "table1".to_string(),
7443 ]);
7444 let result = namespace
7445 .create_table(create_table_req, bytes::Bytes::from(ipc_data))
7446 .await;
7447 assert!(
7448 result.is_ok(),
7449 "Failed to create table in deeply nested namespace"
7450 );
7451
7452 let mut exists_req = TableExistsRequest::new();
7454 exists_req.id = Some(vec![
7455 "level1".to_string(),
7456 "level2".to_string(),
7457 "level3".to_string(),
7458 "table1".to_string(),
7459 ]);
7460 let result = namespace.table_exists(exists_req).await;
7461 assert!(result.is_ok());
7462 }
7463
7464 #[tokio::test]
7465 async fn test_namespace_with_properties() {
7466 let (namespace, _temp_dir) = create_test_namespace().await;
7467
7468 let mut properties = HashMap::new();
7470 properties.insert("owner".to_string(), "test_user".to_string());
7471 properties.insert("description".to_string(), "Test namespace".to_string());
7472
7473 let mut create_req = CreateNamespaceRequest::new();
7474 create_req.id = Some(vec!["test_ns".to_string()]);
7475 create_req.properties = Some(properties.clone());
7476 namespace.create_namespace(create_req).await.unwrap();
7477
7478 let describe_req = DescribeNamespaceRequest {
7480 id: Some(vec!["test_ns".to_string()]),
7481 ..Default::default()
7482 };
7483 let result = namespace.describe_namespace(describe_req).await;
7484 assert!(result.is_ok());
7485 let response = result.unwrap();
7486 assert!(response.properties.is_some());
7487 let props = response.properties.unwrap();
7488 assert_eq!(props.get("owner"), Some(&"test_user".to_string()));
7489 assert_eq!(
7490 props.get("description"),
7491 Some(&"Test namespace".to_string())
7492 );
7493 }
7494
7495 #[tokio::test]
7496 async fn test_cannot_drop_namespace_with_tables() {
7497 let (namespace, _temp_dir) = create_test_namespace().await;
7498
7499 let mut create_ns_req = CreateNamespaceRequest::new();
7501 create_ns_req.id = Some(vec!["test_ns".to_string()]);
7502 namespace.create_namespace(create_ns_req).await.unwrap();
7503
7504 let schema = create_test_schema();
7506 let ipc_data = create_test_ipc_data(&schema);
7507 let mut create_table_req = CreateTableRequest::new();
7508 create_table_req.id = Some(vec!["test_ns".to_string(), "table1".to_string()]);
7509 namespace
7510 .create_table(create_table_req, bytes::Bytes::from(ipc_data))
7511 .await
7512 .unwrap();
7513
7514 let mut drop_req = DropNamespaceRequest::new();
7516 drop_req.id = Some(vec!["test_ns".to_string()]);
7517 let result = namespace.drop_namespace(drop_req).await;
7518 assert!(
7519 result.is_err(),
7520 "Should not be able to drop namespace with tables"
7521 );
7522 }
7523
7524 #[tokio::test]
7525 async fn test_isolation_between_namespaces() {
7526 let (namespace, _temp_dir) = create_test_namespace().await;
7527
7528 let mut create_req = CreateNamespaceRequest::new();
7530 create_req.id = Some(vec!["ns1".to_string()]);
7531 namespace.create_namespace(create_req).await.unwrap();
7532
7533 let mut create_req = CreateNamespaceRequest::new();
7534 create_req.id = Some(vec!["ns2".to_string()]);
7535 namespace.create_namespace(create_req).await.unwrap();
7536
7537 let schema = create_test_schema();
7539 let ipc_data = create_test_ipc_data(&schema);
7540
7541 let mut create_table_req = CreateTableRequest::new();
7542 create_table_req.id = Some(vec!["ns1".to_string(), "table1".to_string()]);
7543 namespace
7544 .create_table(create_table_req, bytes::Bytes::from(ipc_data.clone()))
7545 .await
7546 .unwrap();
7547
7548 let mut create_table_req = CreateTableRequest::new();
7549 create_table_req.id = Some(vec!["ns2".to_string(), "table1".to_string()]);
7550 namespace
7551 .create_table(create_table_req, bytes::Bytes::from(ipc_data))
7552 .await
7553 .unwrap();
7554
7555 let list_req = ListTablesRequest {
7557 id: Some(vec!["ns1".to_string()]),
7558 page_token: None,
7559 limit: None,
7560 ..Default::default()
7561 };
7562 let result = namespace.list_tables(list_req).await.unwrap();
7563 assert_eq!(result.tables.len(), 1);
7564 assert_eq!(result.tables[0], "table1");
7565
7566 let list_req = ListTablesRequest {
7567 id: Some(vec!["ns2".to_string()]),
7568 page_token: None,
7569 limit: None,
7570 ..Default::default()
7571 };
7572 let result = namespace.list_tables(list_req).await.unwrap();
7573 assert_eq!(result.tables.len(), 1);
7574 assert_eq!(result.tables[0], "table1");
7575
7576 let mut drop_req = DropTableRequest::new();
7578 drop_req.id = Some(vec!["ns1".to_string(), "table1".to_string()]);
7579 namespace.drop_table(drop_req).await.unwrap();
7580
7581 let mut exists_req = TableExistsRequest::new();
7583 exists_req.id = Some(vec!["ns1".to_string(), "table1".to_string()]);
7584 assert!(namespace.table_exists(exists_req).await.is_err());
7585
7586 let mut exists_req = TableExistsRequest::new();
7587 exists_req.id = Some(vec!["ns2".to_string(), "table1".to_string()]);
7588 assert!(namespace.table_exists(exists_req).await.is_ok());
7589 }
7590
7591 #[tokio::test]
7592 async fn test_migrate_directory_tables() {
7593 let temp_dir = TempStdDir::default();
7594 let temp_path = temp_dir.to_str().unwrap();
7595
7596 let dir_only_ns = DirectoryNamespaceBuilder::new(temp_path)
7598 .manifest_enabled(false)
7599 .dir_listing_enabled(true)
7600 .build()
7601 .await
7602 .unwrap();
7603
7604 let schema = create_test_schema();
7606 let ipc_data = create_test_ipc_data(&schema);
7607
7608 for i in 1..=3 {
7609 let mut create_req = CreateTableRequest::new();
7610 create_req.id = Some(vec![format!("table{}", i)]);
7611 dir_only_ns
7612 .create_table(create_req, bytes::Bytes::from(ipc_data.clone()))
7613 .await
7614 .unwrap();
7615 }
7616
7617 drop(dir_only_ns);
7618
7619 let dual_mode_ns = DirectoryNamespaceBuilder::new(temp_path)
7621 .manifest_enabled(true)
7622 .dir_listing_enabled(true)
7623 .build()
7624 .await
7625 .unwrap();
7626
7627 let mut list_req = ListTablesRequest::new();
7629 list_req.id = Some(vec![]);
7630 let tables = dual_mode_ns.list_tables(list_req).await.unwrap().tables;
7631 assert_eq!(tables.len(), 3);
7632
7633 let migrated_count = dual_mode_ns.migrate().await.unwrap();
7635 assert_eq!(migrated_count, 3, "Should migrate all 3 tables");
7636
7637 let mut list_req = ListTablesRequest::new();
7639 list_req.id = Some(vec![]);
7640 let tables = dual_mode_ns.list_tables(list_req).await.unwrap().tables;
7641 assert_eq!(tables.len(), 3);
7642
7643 let migrated_count = dual_mode_ns.migrate().await.unwrap();
7645 assert_eq!(
7646 migrated_count, 0,
7647 "Should not migrate already-migrated tables"
7648 );
7649
7650 drop(dual_mode_ns);
7651
7652 let manifest_only_ns = DirectoryNamespaceBuilder::new(temp_path)
7654 .manifest_enabled(true)
7655 .dir_listing_enabled(false)
7656 .build()
7657 .await
7658 .unwrap();
7659
7660 let mut list_req = ListTablesRequest::new();
7662 list_req.id = Some(vec![]);
7663 let tables = manifest_only_ns.list_tables(list_req).await.unwrap().tables;
7664 assert_eq!(tables.len(), 3);
7665 assert!(tables.contains(&"table1".to_string()));
7666 assert!(tables.contains(&"table2".to_string()));
7667 assert!(tables.contains(&"table3".to_string()));
7668 }
7669
7670 #[tokio::test]
7671 async fn test_migrate_without_manifest() {
7672 let temp_dir = TempStdDir::default();
7673 let temp_path = temp_dir.to_str().unwrap();
7674
7675 let namespace = DirectoryNamespaceBuilder::new(temp_path)
7677 .manifest_enabled(false)
7678 .dir_listing_enabled(true)
7679 .build()
7680 .await
7681 .unwrap();
7682
7683 let migrated_count = namespace.migrate().await.unwrap();
7685 assert_eq!(migrated_count, 0);
7686 }
7687
7688 #[tokio::test]
7689 async fn test_register_table() {
7690 use lance_namespace::models::{RegisterTableRequest, TableExistsRequest};
7691
7692 let temp_dir = TempStdDir::default();
7693 let temp_path = temp_dir.to_str().unwrap();
7694
7695 let namespace = DirectoryNamespaceBuilder::new(temp_path)
7696 .dir_listing_to_manifest_migration_enabled(true)
7697 .build()
7698 .await
7699 .unwrap();
7700
7701 let schema = create_test_schema();
7703 let ipc_data = create_test_ipc_data(&schema);
7704
7705 let table_uri = format!("{}/external_table.lance", temp_path);
7706 let cursor = Cursor::new(ipc_data);
7707 let stream_reader = StreamReader::try_new(cursor, None).unwrap();
7708 let batches: Vec<_> = stream_reader
7709 .collect::<std::result::Result<Vec<_>, _>>()
7710 .unwrap();
7711 let schema = batches[0].schema();
7712 let batch_results: Vec<_> = batches.into_iter().map(Ok).collect();
7713 let reader = RecordBatchIterator::new(batch_results, schema);
7714 Dataset::write(Box::new(reader), &table_uri, None)
7715 .await
7716 .unwrap();
7717
7718 let mut register_req = RegisterTableRequest::new("external_table.lance".to_string());
7720 register_req.id = Some(vec!["registered_table".to_string()]);
7721
7722 let response = namespace.register_table(register_req).await.unwrap();
7723 assert_eq!(response.location, Some("external_table.lance".to_string()));
7724
7725 let mut exists_req = TableExistsRequest::new();
7727 exists_req.id = Some(vec!["registered_table".to_string()]);
7728 assert!(namespace.table_exists(exists_req).await.is_ok());
7729
7730 let mut list_req = ListTablesRequest::new();
7732 list_req.id = Some(vec![]);
7733 let tables = namespace.list_tables(list_req).await.unwrap();
7734 assert!(tables.tables.contains(&"registered_table".to_string()));
7735 }
7736
7737 #[tokio::test]
7738 async fn test_register_table_duplicate_fails() {
7739 use lance_namespace::models::RegisterTableRequest;
7740
7741 let temp_dir = TempStdDir::default();
7742 let temp_path = temp_dir.to_str().unwrap();
7743
7744 let namespace = DirectoryNamespaceBuilder::new(temp_path)
7745 .build()
7746 .await
7747 .unwrap();
7748
7749 let mut register_req = RegisterTableRequest::new("test_table.lance".to_string());
7751 register_req.id = Some(vec!["test_table".to_string()]);
7752
7753 namespace
7754 .register_table(register_req.clone())
7755 .await
7756 .unwrap();
7757
7758 let result = namespace.register_table(register_req).await;
7760 assert!(result.is_err());
7761 assert!(result.unwrap_err().to_string().contains("already exists"));
7762 }
7763
7764 #[tokio::test]
7765 async fn test_deregister_table() {
7766 use lance_namespace::models::{DeregisterTableRequest, TableExistsRequest};
7767
7768 let temp_dir = TempStdDir::default();
7769 let temp_path = temp_dir.to_str().unwrap();
7770
7771 let namespace = DirectoryNamespaceBuilder::new(temp_path)
7774 .manifest_enabled(true)
7775 .dir_listing_enabled(false)
7776 .build()
7777 .await
7778 .unwrap();
7779
7780 let schema = create_test_schema();
7782 let ipc_data = create_test_ipc_data(&schema);
7783
7784 let mut create_req = CreateTableRequest::new();
7785 create_req.id = Some(vec!["test_table".to_string()]);
7786 namespace
7787 .create_table(create_req, bytes::Bytes::from(ipc_data))
7788 .await
7789 .unwrap();
7790
7791 let mut exists_req = TableExistsRequest::new();
7793 exists_req.id = Some(vec!["test_table".to_string()]);
7794 assert!(namespace.table_exists(exists_req.clone()).await.is_ok());
7795
7796 let mut deregister_req = DeregisterTableRequest::new();
7798 deregister_req.id = Some(vec!["test_table".to_string()]);
7799 let response = namespace.deregister_table(deregister_req).await.unwrap();
7800
7801 assert!(
7803 response.location.is_some(),
7804 "Deregister should return location"
7805 );
7806 let location = response.location.as_ref().unwrap();
7807 let expected_url = lance_io::object_store::uri_to_url(temp_path)
7810 .expect("Failed to convert temp path to URL");
7811 let expected_prefix = expected_url.to_string();
7812 assert!(
7813 location.starts_with(&expected_prefix),
7814 "Location should start with '{}', got: {}",
7815 expected_prefix,
7816 location
7817 );
7818 assert!(
7819 location.contains("test_table"),
7820 "Location should contain table name: {}",
7821 location
7822 );
7823 assert_eq!(response.id, Some(vec!["test_table".to_string()]));
7824
7825 assert!(namespace.table_exists(exists_req).await.is_err());
7827
7828 let dataset = Dataset::open(location).await;
7830 assert!(
7831 dataset.is_ok(),
7832 "Physical table data should still exist at {}",
7833 location
7834 );
7835 }
7836
7837 #[tokio::test]
7838 async fn test_deregister_table_in_child_namespace() {
7839 use lance_namespace::models::{
7840 CreateNamespaceRequest, DeregisterTableRequest, TableExistsRequest,
7841 };
7842
7843 let temp_dir = TempStdDir::default();
7844 let temp_path = temp_dir.to_str().unwrap();
7845
7846 let namespace = DirectoryNamespaceBuilder::new(temp_path)
7847 .build()
7848 .await
7849 .unwrap();
7850
7851 let mut create_ns_req = CreateNamespaceRequest::new();
7853 create_ns_req.id = Some(vec!["test_ns".to_string()]);
7854 namespace.create_namespace(create_ns_req).await.unwrap();
7855
7856 let schema = create_test_schema();
7858 let ipc_data = create_test_ipc_data(&schema);
7859
7860 let mut create_req = CreateTableRequest::new();
7861 create_req.id = Some(vec!["test_ns".to_string(), "test_table".to_string()]);
7862 namespace
7863 .create_table(create_req, bytes::Bytes::from(ipc_data))
7864 .await
7865 .unwrap();
7866
7867 let mut deregister_req = DeregisterTableRequest::new();
7869 deregister_req.id = Some(vec!["test_ns".to_string(), "test_table".to_string()]);
7870 let response = namespace.deregister_table(deregister_req).await.unwrap();
7871
7872 assert!(
7874 response.location.is_some(),
7875 "Deregister should return location"
7876 );
7877 let location = response.location.as_ref().unwrap();
7878 let expected_url = lance_io::object_store::uri_to_url(temp_path)
7881 .expect("Failed to convert temp path to URL");
7882 let expected_prefix = expected_url.to_string();
7883 assert!(
7884 location.starts_with(&expected_prefix),
7885 "Location should start with '{}', got: {}",
7886 expected_prefix,
7887 location
7888 );
7889 assert!(
7890 location.contains("test_ns") && location.contains("test_table"),
7891 "Location should contain namespace and table name: {}",
7892 location
7893 );
7894 assert_eq!(
7895 response.id,
7896 Some(vec!["test_ns".to_string(), "test_table".to_string()])
7897 );
7898
7899 let mut exists_req = TableExistsRequest::new();
7901 exists_req.id = Some(vec!["test_ns".to_string(), "test_table".to_string()]);
7902 assert!(namespace.table_exists(exists_req).await.is_err());
7903 }
7904
7905 #[tokio::test]
7906 async fn test_register_without_manifest_fails() {
7907 use lance_namespace::models::RegisterTableRequest;
7908
7909 let temp_dir = TempStdDir::default();
7910 let temp_path = temp_dir.to_str().unwrap();
7911
7912 let namespace = DirectoryNamespaceBuilder::new(temp_path)
7914 .manifest_enabled(false)
7915 .build()
7916 .await
7917 .unwrap();
7918
7919 let mut register_req = RegisterTableRequest::new("test_table.lance".to_string());
7921 register_req.id = Some(vec!["test_table".to_string()]);
7922 let result = namespace.register_table(register_req).await;
7923 assert!(result.is_err());
7924 assert!(
7925 result
7926 .unwrap_err()
7927 .to_string()
7928 .contains("manifest mode is enabled")
7929 );
7930
7931 }
7934
7935 #[tokio::test]
7936 async fn test_register_table_rejects_absolute_uri() {
7937 use lance_namespace::models::RegisterTableRequest;
7938
7939 let temp_dir = TempStdDir::default();
7940 let temp_path = temp_dir.to_str().unwrap();
7941
7942 let namespace = DirectoryNamespaceBuilder::new(temp_path)
7943 .build()
7944 .await
7945 .unwrap();
7946
7947 let mut register_req = RegisterTableRequest::new("s3://bucket/table.lance".to_string());
7949 register_req.id = Some(vec!["test_table".to_string()]);
7950 let result = namespace.register_table(register_req).await;
7951 assert!(result.is_err());
7952 let err_msg = result.unwrap_err().to_string();
7953 assert!(err_msg.contains("Absolute URIs are not allowed"));
7954 }
7955
7956 #[tokio::test]
7957 async fn test_register_table_rejects_absolute_path() {
7958 use lance_namespace::models::RegisterTableRequest;
7959
7960 let temp_dir = TempStdDir::default();
7961 let temp_path = temp_dir.to_str().unwrap();
7962
7963 let namespace = DirectoryNamespaceBuilder::new(temp_path)
7964 .build()
7965 .await
7966 .unwrap();
7967
7968 let mut register_req = RegisterTableRequest::new("/tmp/table.lance".to_string());
7970 register_req.id = Some(vec!["test_table".to_string()]);
7971 let result = namespace.register_table(register_req).await;
7972 assert!(result.is_err());
7973 let err_msg = result.unwrap_err().to_string();
7974 assert!(err_msg.contains("Absolute paths are not allowed"));
7975 }
7976
7977 #[tokio::test]
7978 async fn test_register_table_rejects_path_traversal() {
7979 use lance_namespace::models::RegisterTableRequest;
7980
7981 let temp_dir = TempStdDir::default();
7982 let temp_path = temp_dir.to_str().unwrap();
7983
7984 let namespace = DirectoryNamespaceBuilder::new(temp_path)
7985 .build()
7986 .await
7987 .unwrap();
7988
7989 let mut register_req = RegisterTableRequest::new("../outside/table.lance".to_string());
7991 register_req.id = Some(vec!["test_table".to_string()]);
7992 let result = namespace.register_table(register_req).await;
7993 assert!(result.is_err());
7994 let err_msg = result.unwrap_err().to_string();
7995 assert!(err_msg.contains("Path traversal is not allowed"));
7996 }
7997
7998 #[tokio::test]
7999 async fn test_namespace_write() {
8000 use arrow::array::Int32Array;
8001 use arrow::datatypes::{DataType, Field as ArrowField, Schema as ArrowSchema};
8002 use arrow::record_batch::{RecordBatch, RecordBatchIterator};
8003 use lance::dataset::{Dataset, WriteMode, WriteParams};
8004 use lance_namespace::LanceNamespace;
8005
8006 let (namespace, _temp_dir) = create_test_namespace().await;
8007 let namespace = Arc::new(namespace) as Arc<dyn LanceNamespace>;
8008
8009 let table_id = vec!["test_ns".to_string(), "test_table".to_string()];
8011 let schema = Arc::new(ArrowSchema::new(vec![
8012 ArrowField::new("a", DataType::Int32, false),
8013 ArrowField::new("b", DataType::Int32, false),
8014 ]));
8015
8016 let data1 = RecordBatch::try_new(
8018 schema.clone(),
8019 vec![
8020 Arc::new(Int32Array::from(vec![1, 2, 3])),
8021 Arc::new(Int32Array::from(vec![10, 20, 30])),
8022 ],
8023 )
8024 .unwrap();
8025
8026 let reader1 = RecordBatchIterator::new(vec![data1].into_iter().map(Ok), schema.clone());
8027 let dataset =
8028 Dataset::write_into_namespace(reader1, namespace.clone(), table_id.clone(), None)
8029 .await
8030 .unwrap();
8031
8032 assert_eq!(dataset.count_rows(None).await.unwrap(), 3);
8033 assert_eq!(dataset.version().version, 1);
8034
8035 let data2 = RecordBatch::try_new(
8037 schema.clone(),
8038 vec![
8039 Arc::new(Int32Array::from(vec![4, 5])),
8040 Arc::new(Int32Array::from(vec![40, 50])),
8041 ],
8042 )
8043 .unwrap();
8044
8045 let params_append = WriteParams {
8046 mode: WriteMode::Append,
8047 ..Default::default()
8048 };
8049
8050 let reader2 = RecordBatchIterator::new(vec![data2].into_iter().map(Ok), schema.clone());
8051 let dataset = Dataset::write_into_namespace(
8052 reader2,
8053 namespace.clone(),
8054 table_id.clone(),
8055 Some(params_append),
8056 )
8057 .await
8058 .unwrap();
8059
8060 assert_eq!(dataset.count_rows(None).await.unwrap(), 5);
8061 assert_eq!(dataset.version().version, 2);
8062
8063 let data3 = RecordBatch::try_new(
8065 schema.clone(),
8066 vec![
8067 Arc::new(Int32Array::from(vec![100, 200])),
8068 Arc::new(Int32Array::from(vec![1000, 2000])),
8069 ],
8070 )
8071 .unwrap();
8072
8073 let params_overwrite = WriteParams {
8074 mode: WriteMode::Overwrite,
8075 ..Default::default()
8076 };
8077
8078 let reader3 = RecordBatchIterator::new(vec![data3].into_iter().map(Ok), schema.clone());
8079 let dataset = Dataset::write_into_namespace(
8080 reader3,
8081 namespace.clone(),
8082 table_id.clone(),
8083 Some(params_overwrite),
8084 )
8085 .await
8086 .unwrap();
8087
8088 assert_eq!(dataset.count_rows(None).await.unwrap(), 2);
8089 assert_eq!(dataset.version().version, 3);
8090
8091 let result = dataset.scan().try_into_batch().await.unwrap();
8093 let a_col = result
8094 .column_by_name("a")
8095 .unwrap()
8096 .as_any()
8097 .downcast_ref::<Int32Array>()
8098 .unwrap();
8099 assert_eq!(a_col.values(), &[100, 200]);
8100 }
8101
8102 #[tokio::test]
8107 async fn test_declare_table_v1_mode() {
8108 use lance_namespace::models::{
8109 DeclareTableRequest, DescribeTableRequest, ListTablesRequest, TableExistsRequest,
8110 };
8111
8112 let temp_dir = TempStdDir::default();
8113 let temp_path = temp_dir.to_str().unwrap();
8114
8115 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8117 .manifest_enabled(false)
8118 .build()
8119 .await
8120 .unwrap();
8121
8122 let mut declare_req = DeclareTableRequest::new();
8124 declare_req.id = Some(vec!["test_table".to_string()]);
8125 let response = namespace.declare_table(declare_req).await.unwrap();
8126
8127 assert!(response.location.is_some());
8129 let location = response.location.as_ref().unwrap();
8130 assert!(location.ends_with("test_table.lance"));
8131
8132 let mut exists_req = TableExistsRequest::new();
8134 exists_req.id = Some(vec!["test_table".to_string()]);
8135 assert!(namespace.table_exists(exists_req).await.is_ok());
8136
8137 let mut describe_req = DescribeTableRequest::new();
8139 describe_req.id = Some(vec!["test_table".to_string()]);
8140 let describe_response = namespace.describe_table(describe_req).await.unwrap();
8141 assert!(describe_response.location.is_some());
8142 assert!(describe_response.version.is_none()); assert!(describe_response.schema.is_none()); assert_eq!(describe_response.is_only_declared, None);
8145
8146 let mut describe_req = DescribeTableRequest::new();
8147 describe_req.id = Some(vec!["test_table".to_string()]);
8148 describe_req.check_declared = Some(true);
8149 let describe_response = namespace.describe_table(describe_req).await.unwrap();
8150 assert_eq!(describe_response.is_only_declared, Some(true));
8151
8152 let mut list_req = ListTablesRequest::new();
8153 list_req.id = Some(vec![]);
8154 let list_response = namespace.list_tables(list_req.clone()).await.unwrap();
8155 assert_eq!(list_response.tables, vec!["test_table".to_string()]);
8156
8157 list_req.include_declared = Some(false);
8158 let list_response = namespace.list_tables(list_req).await.unwrap();
8159 assert!(list_response.tables.is_empty());
8160 }
8161
8162 #[tokio::test]
8163 async fn test_insert_into_declared_table_promotes_it_from_declared_state() {
8164 use lance_namespace::models::{
8165 DeclareTableRequest, DescribeTableRequest, InsertIntoTableRequest,
8166 };
8167
8168 let temp_dir = TempStdDir::default();
8169 let temp_path = temp_dir.to_str().unwrap();
8170
8171 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8172 .manifest_enabled(false)
8173 .build()
8174 .await
8175 .unwrap();
8176
8177 let mut declare_req = DeclareTableRequest::new();
8178 declare_req.id = Some(vec!["test_table".to_string()]);
8179 namespace.declare_table(declare_req).await.unwrap();
8180
8181 let schema = create_test_schema();
8182 let ipc_data = create_test_ipc_data(&schema);
8183 let mut insert_req = InsertIntoTableRequest::new();
8184 insert_req.id = Some(vec!["test_table".to_string()]);
8185 namespace
8186 .insert_into_table(insert_req, bytes::Bytes::from(ipc_data))
8187 .await
8188 .unwrap();
8189
8190 let mut describe_req = DescribeTableRequest::new();
8191 describe_req.id = Some(vec!["test_table".to_string()]);
8192 describe_req.load_detailed_metadata = Some(true);
8193 let describe_response = namespace.describe_table(describe_req).await.unwrap();
8194
8195 assert_eq!(describe_response.is_only_declared, Some(false));
8196 assert_eq!(describe_response.version, Some(1));
8197 assert!(describe_response.schema.is_some());
8198
8199 let mut list_req = ListTablesRequest::new();
8200 list_req.id = Some(vec![]);
8201 list_req.include_declared = Some(false);
8202 assert_eq!(
8203 namespace.list_tables(list_req).await.unwrap().tables,
8204 vec!["test_table".to_string()]
8205 );
8206 }
8207
8208 #[tokio::test]
8209 async fn test_create_table_after_declare_table_v1_mode_creates_table() {
8210 use lance_namespace::models::{
8211 DeclareTableRequest, DescribeTableRequest, ListTablesRequest,
8212 };
8213
8214 let temp_dir = TempStdDir::default();
8215 let temp_path = temp_dir.to_str().unwrap();
8216
8217 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8218 .manifest_enabled(false)
8219 .build()
8220 .await
8221 .unwrap();
8222
8223 let mut declare_req = DeclareTableRequest::new();
8224 declare_req.id = Some(vec!["test_table".to_string()]);
8225 namespace.declare_table(declare_req).await.unwrap();
8226
8227 let mut create_req = CreateTableRequest::new();
8228 create_req.id = Some(vec!["test_table".to_string()]);
8229 let response = namespace
8230 .create_table(
8231 create_req,
8232 bytes::Bytes::from(create_non_empty_test_ipc_data()),
8233 )
8234 .await
8235 .unwrap();
8236
8237 assert_eq!(response.version, Some(1));
8238
8239 let mut describe_req = DescribeTableRequest::new();
8240 describe_req.id = Some(vec!["test_table".to_string()]);
8241 describe_req.load_detailed_metadata = Some(true);
8242 let describe_response = namespace.describe_table(describe_req).await.unwrap();
8243 assert_eq!(describe_response.is_only_declared, Some(false));
8244 assert_eq!(describe_response.version, Some(1));
8245
8246 let mut list_req = ListTablesRequest::new();
8247 list_req.id = Some(vec![]);
8248 list_req.include_declared = Some(false);
8249 assert_eq!(
8250 namespace.list_tables(list_req).await.unwrap().tables,
8251 vec!["test_table".to_string()]
8252 );
8253 }
8254
8255 #[tokio::test]
8256 async fn test_insert_into_declared_table_with_manifest_promotes_it() {
8257 use lance_namespace::models::{
8258 DeclareTableRequest, DescribeTableRequest, InsertIntoTableRequest, ListTablesRequest,
8259 };
8260
8261 let temp_dir = TempStdDir::default();
8262 let temp_path = temp_dir.to_str().unwrap();
8263
8264 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8265 .manifest_enabled(true)
8266 .dir_listing_enabled(false)
8267 .build()
8268 .await
8269 .unwrap();
8270
8271 let mut declare_req = DeclareTableRequest::new();
8272 declare_req.id = Some(vec!["test_table".to_string()]);
8273 namespace.declare_table(declare_req).await.unwrap();
8274
8275 let mut insert_req = InsertIntoTableRequest::new();
8276 insert_req.id = Some(vec!["test_table".to_string()]);
8277 namespace
8278 .insert_into_table(
8279 insert_req,
8280 bytes::Bytes::from(create_non_empty_test_ipc_data()),
8281 )
8282 .await
8283 .unwrap();
8284
8285 let mut describe_req = DescribeTableRequest::new();
8286 describe_req.id = Some(vec!["test_table".to_string()]);
8287 describe_req.load_detailed_metadata = Some(true);
8288 let describe_response = namespace.describe_table(describe_req).await.unwrap();
8289 assert_eq!(describe_response.is_only_declared, Some(false));
8290 assert_eq!(describe_response.version, Some(1));
8291
8292 let mut list_req = ListTablesRequest::new();
8293 list_req.id = Some(vec![]);
8294 list_req.include_declared = Some(false);
8295 assert_eq!(
8296 namespace.list_tables(list_req).await.unwrap().tables,
8297 vec!["test_table".to_string()]
8298 );
8299 }
8300
8301 #[tokio::test]
8302 async fn test_create_table_after_declare_table_with_manifest_creates_table() {
8303 use lance_namespace::models::{
8304 CreateTableRequest, DeclareTableRequest, DescribeTableRequest, ListTablesRequest,
8305 };
8306
8307 let temp_dir = TempStdDir::default();
8308 let temp_path = temp_dir.to_str().unwrap();
8309
8310 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8311 .manifest_enabled(true)
8312 .dir_listing_enabled(false)
8313 .build()
8314 .await
8315 .unwrap();
8316
8317 let mut declare_req = DeclareTableRequest::new();
8318 declare_req.id = Some(vec!["test_table".to_string()]);
8319 declare_req.properties = Some(HashMap::from([("owner".to_string(), "alice".to_string())]));
8320 namespace.declare_table(declare_req).await.unwrap();
8321
8322 let mut create_req = CreateTableRequest::new();
8323 create_req.id = Some(vec!["test_table".to_string()]);
8324 create_req.mode = Some("Overwrite".to_string());
8325 let response = namespace
8326 .create_table(
8327 create_req,
8328 bytes::Bytes::from(create_non_empty_test_ipc_data()),
8329 )
8330 .await
8331 .unwrap();
8332
8333 assert_eq!(response.version, Some(1));
8334 assert_eq!(
8335 response
8336 .properties
8337 .as_ref()
8338 .and_then(|properties| properties.get("owner")),
8339 Some(&"alice".to_string())
8340 );
8341
8342 let mut describe_req = DescribeTableRequest::new();
8343 describe_req.id = Some(vec!["test_table".to_string()]);
8344 describe_req.load_detailed_metadata = Some(true);
8345 let describe_response = namespace.describe_table(describe_req).await.unwrap();
8346 assert_eq!(describe_response.is_only_declared, Some(false));
8347 assert_eq!(describe_response.version, Some(1));
8348 assert_eq!(
8349 describe_response
8350 .properties
8351 .as_ref()
8352 .and_then(|properties| properties.get("owner")),
8353 Some(&"alice".to_string())
8354 );
8355
8356 let mut list_req = ListTablesRequest::new();
8357 list_req.id = Some(vec![]);
8358 list_req.include_declared = Some(false);
8359 assert_eq!(
8360 namespace.list_tables(list_req).await.unwrap().tables,
8361 vec!["test_table".to_string()]
8362 );
8363 }
8364
8365 #[tokio::test]
8366 async fn test_create_table_after_declare_table_with_manifest_rejects_new_properties() {
8367 use lance_namespace::models::{CreateTableRequest, DeclareTableRequest};
8368
8369 let temp_dir = TempStdDir::default();
8370 let temp_path = temp_dir.to_str().unwrap();
8371
8372 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8373 .manifest_enabled(true)
8374 .dir_listing_enabled(false)
8375 .build()
8376 .await
8377 .unwrap();
8378
8379 let mut declare_req = DeclareTableRequest::new();
8380 declare_req.id = Some(vec!["test_table".to_string()]);
8381 declare_req.properties = Some(HashMap::from([("owner".to_string(), "alice".to_string())]));
8382 namespace.declare_table(declare_req).await.unwrap();
8383
8384 let mut create_req = CreateTableRequest::new();
8385 create_req.id = Some(vec!["test_table".to_string()]);
8386 create_req.properties = Some(HashMap::from([("owner".to_string(), "bob".to_string())]));
8387
8388 let result = namespace
8389 .create_table(
8390 create_req,
8391 bytes::Bytes::from(create_non_empty_test_ipc_data()),
8392 )
8393 .await;
8394
8395 assert!(result.is_err());
8396 assert!(
8397 result
8398 .unwrap_err()
8399 .to_string()
8400 .contains("cannot set properties for already declared table")
8401 );
8402 }
8403
8404 #[tokio::test]
8405 async fn test_create_table_with_manifest_exist_ok_keeps_existing_table() {
8406 use lance_namespace::models::{CreateTableRequest, DescribeTableRequest};
8407
8408 let temp_dir = TempStdDir::default();
8409 let temp_path = temp_dir.to_str().unwrap();
8410
8411 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8412 .manifest_enabled(true)
8413 .dir_listing_enabled(false)
8414 .build()
8415 .await
8416 .unwrap();
8417
8418 let mut create_req = CreateTableRequest::new();
8419 create_req.id = Some(vec!["test_table".to_string()]);
8420 create_req.properties = Some(HashMap::from([("owner".to_string(), "alice".to_string())]));
8421 namespace
8422 .create_table(
8423 create_req,
8424 bytes::Bytes::from(create_non_empty_test_ipc_data()),
8425 )
8426 .await
8427 .unwrap();
8428
8429 let mut create_req = CreateTableRequest::new();
8430 create_req.id = Some(vec!["test_table".to_string()]);
8431 create_req.mode = Some("ExistOk".to_string());
8432 create_req.properties = Some(HashMap::from([("owner".to_string(), "bob".to_string())]));
8433 let response = namespace
8434 .create_table(
8435 create_req,
8436 bytes::Bytes::from(create_single_row_test_ipc_data()),
8437 )
8438 .await
8439 .unwrap();
8440
8441 assert_eq!(
8442 response
8443 .properties
8444 .as_ref()
8445 .and_then(|properties| properties.get("owner")),
8446 Some(&"alice".to_string())
8447 );
8448 assert_eq!(
8449 open_dataset(&namespace, "test_table")
8450 .await
8451 .count_rows(None)
8452 .await
8453 .unwrap(),
8454 2
8455 );
8456
8457 let mut describe_req = DescribeTableRequest::new();
8458 describe_req.id = Some(vec!["test_table".to_string()]);
8459 let describe_response = namespace.describe_table(describe_req).await.unwrap();
8460 assert_eq!(
8461 describe_response
8462 .properties
8463 .as_ref()
8464 .and_then(|properties| properties.get("owner")),
8465 Some(&"alice".to_string())
8466 );
8467 }
8468
8469 #[tokio::test]
8470 async fn test_create_table_with_manifest_overwrite_replaces_existing_table() {
8471 use lance_namespace::models::{CreateTableRequest, DescribeTableRequest};
8472
8473 let temp_dir = TempStdDir::default();
8474 let temp_path = temp_dir.to_str().unwrap();
8475
8476 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8477 .manifest_enabled(true)
8478 .dir_listing_enabled(false)
8479 .build()
8480 .await
8481 .unwrap();
8482
8483 let mut create_req = CreateTableRequest::new();
8484 create_req.id = Some(vec!["test_table".to_string()]);
8485 create_req.properties = Some(HashMap::from([("owner".to_string(), "alice".to_string())]));
8486 namespace
8487 .create_table(
8488 create_req,
8489 bytes::Bytes::from(create_non_empty_test_ipc_data()),
8490 )
8491 .await
8492 .unwrap();
8493
8494 let mut create_req = CreateTableRequest::new();
8495 create_req.id = Some(vec!["test_table".to_string()]);
8496 create_req.mode = Some("overwrite".to_string());
8497 create_req.properties = Some(HashMap::from([("owner".to_string(), "bob".to_string())]));
8498 let response = namespace
8499 .create_table(
8500 create_req,
8501 bytes::Bytes::from(create_single_row_test_ipc_data()),
8502 )
8503 .await
8504 .unwrap();
8505
8506 assert_eq!(response.version, Some(2));
8507 assert_eq!(
8508 response
8509 .properties
8510 .as_ref()
8511 .and_then(|properties| properties.get("owner")),
8512 Some(&"bob".to_string())
8513 );
8514 assert_eq!(
8515 open_dataset(&namespace, "test_table")
8516 .await
8517 .count_rows(None)
8518 .await
8519 .unwrap(),
8520 1
8521 );
8522
8523 let mut describe_req = DescribeTableRequest::new();
8524 describe_req.id = Some(vec!["test_table".to_string()]);
8525 let describe_response = namespace.describe_table(describe_req).await.unwrap();
8526 assert_eq!(
8527 describe_response
8528 .properties
8529 .as_ref()
8530 .and_then(|properties| properties.get("owner")),
8531 Some(&"bob".to_string())
8532 );
8533 }
8534
8535 #[tokio::test]
8536 async fn test_create_table_with_manifest_invalid_mode_rejected() {
8537 use lance_namespace::models::CreateTableRequest;
8538
8539 let temp_dir = TempStdDir::default();
8540 let temp_path = temp_dir.to_str().unwrap();
8541
8542 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8543 .manifest_enabled(true)
8544 .dir_listing_enabled(false)
8545 .build()
8546 .await
8547 .unwrap();
8548
8549 let mut create_req = CreateTableRequest::new();
8550 create_req.id = Some(vec!["test_table".to_string()]);
8551 create_req.mode = Some("append".to_string());
8552 let result = namespace
8553 .create_table(
8554 create_req,
8555 bytes::Bytes::from(create_non_empty_test_ipc_data()),
8556 )
8557 .await;
8558
8559 assert!(result.is_err());
8560 assert!(
8561 result
8562 .unwrap_err()
8563 .to_string()
8564 .contains("Unsupported create_table mode")
8565 );
8566 }
8567
8568 #[tokio::test]
8569 async fn test_merge_insert_into_declared_table_v1_mode_creates_table() {
8570 use lance_namespace::models::{
8571 DeclareTableRequest, DescribeTableRequest, ListTablesRequest,
8572 MergeInsertIntoTableRequest,
8573 };
8574
8575 let temp_dir = TempStdDir::default();
8576 let temp_path = temp_dir.to_str().unwrap();
8577
8578 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8579 .manifest_enabled(false)
8580 .build()
8581 .await
8582 .unwrap();
8583
8584 let mut declare_req = DeclareTableRequest::new();
8585 declare_req.id = Some(vec!["test_table".to_string()]);
8586 namespace.declare_table(declare_req).await.unwrap();
8587
8588 let mut merge_req = MergeInsertIntoTableRequest::new();
8589 merge_req.id = Some(vec!["test_table".to_string()]);
8590 merge_req.on = Some("id".to_string());
8591 let response = namespace
8592 .merge_insert_into_table(
8593 merge_req,
8594 bytes::Bytes::from(create_non_empty_test_ipc_data()),
8595 )
8596 .await
8597 .unwrap();
8598
8599 assert_eq!(response.num_inserted_rows, Some(2));
8600 assert_eq!(response.num_updated_rows, Some(0));
8601
8602 let mut describe_req = DescribeTableRequest::new();
8603 describe_req.id = Some(vec!["test_table".to_string()]);
8604 describe_req.load_detailed_metadata = Some(true);
8605 let describe_response = namespace.describe_table(describe_req).await.unwrap();
8606 assert_eq!(describe_response.is_only_declared, Some(false));
8607 assert_eq!(describe_response.version, Some(1));
8608
8609 let mut list_req = ListTablesRequest::new();
8610 list_req.id = Some(vec![]);
8611 list_req.include_declared = Some(false);
8612 assert_eq!(
8613 namespace.list_tables(list_req).await.unwrap().tables,
8614 vec!["test_table".to_string()]
8615 );
8616 }
8617
8618 #[tokio::test]
8619 async fn test_merge_insert_into_declared_table_with_manifest_creates_table() {
8620 use lance_namespace::models::{
8621 DeclareTableRequest, DescribeTableRequest, ListTablesRequest,
8622 MergeInsertIntoTableRequest,
8623 };
8624
8625 let temp_dir = TempStdDir::default();
8626 let temp_path = temp_dir.to_str().unwrap();
8627
8628 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8629 .manifest_enabled(true)
8630 .dir_listing_enabled(false)
8631 .build()
8632 .await
8633 .unwrap();
8634
8635 let mut declare_req = DeclareTableRequest::new();
8636 declare_req.id = Some(vec!["test_table".to_string()]);
8637 namespace.declare_table(declare_req).await.unwrap();
8638
8639 let mut merge_req = MergeInsertIntoTableRequest::new();
8640 merge_req.id = Some(vec!["test_table".to_string()]);
8641 merge_req.on = Some("id".to_string());
8642 let response = namespace
8643 .merge_insert_into_table(
8644 merge_req,
8645 bytes::Bytes::from(create_non_empty_test_ipc_data()),
8646 )
8647 .await
8648 .unwrap();
8649
8650 assert_eq!(response.num_inserted_rows, Some(2));
8651 assert_eq!(response.num_updated_rows, Some(0));
8652
8653 let mut describe_req = DescribeTableRequest::new();
8654 describe_req.id = Some(vec!["test_table".to_string()]);
8655 describe_req.load_detailed_metadata = Some(true);
8656 let describe_response = namespace.describe_table(describe_req).await.unwrap();
8657 assert_eq!(describe_response.is_only_declared, Some(false));
8658 assert_eq!(describe_response.version, Some(1));
8659
8660 let mut list_req = ListTablesRequest::new();
8661 list_req.id = Some(vec![]);
8662 list_req.include_declared = Some(false);
8663 assert_eq!(
8664 namespace.list_tables(list_req).await.unwrap().tables,
8665 vec!["test_table".to_string()]
8666 );
8667 }
8668
8669 #[tokio::test]
8670 async fn test_declare_table_with_manifest() {
8671 use lance_namespace::models::{
8672 DeclareTableRequest, DescribeTableRequest, ListTablesRequest, TableExistsRequest,
8673 };
8674
8675 let temp_dir = TempStdDir::default();
8676 let temp_path = temp_dir.to_str().unwrap();
8677
8678 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8680 .manifest_enabled(true)
8681 .dir_listing_enabled(false)
8682 .build()
8683 .await
8684 .unwrap();
8685
8686 let mut declare_req = DeclareTableRequest::new();
8688 declare_req.id = Some(vec!["test_table".to_string()]);
8689 declare_req.properties = Some(HashMap::from([("owner".to_string(), "alice".to_string())]));
8690 let response = namespace.declare_table(declare_req).await.unwrap();
8691
8692 assert!(response.location.is_some());
8694 assert_eq!(
8695 response
8696 .properties
8697 .as_ref()
8698 .and_then(|properties| properties.get("owner")),
8699 Some(&"alice".to_string())
8700 );
8701
8702 let mut exists_req = TableExistsRequest::new();
8704 exists_req.id = Some(vec!["test_table".to_string()]);
8705 assert!(namespace.table_exists(exists_req).await.is_ok());
8706
8707 let mut describe_req = DescribeTableRequest::new();
8708 describe_req.id = Some(vec!["test_table".to_string()]);
8709 let describe_response = namespace.describe_table(describe_req).await.unwrap();
8710 assert_eq!(describe_response.is_only_declared, None);
8711
8712 let mut describe_req = DescribeTableRequest::new();
8713 describe_req.id = Some(vec!["test_table".to_string()]);
8714 describe_req.check_declared = Some(true);
8715 let describe_response = namespace.describe_table(describe_req).await.unwrap();
8716 assert_eq!(describe_response.is_only_declared, Some(true));
8717 assert_eq!(
8718 describe_response
8719 .properties
8720 .as_ref()
8721 .and_then(|properties| properties.get("owner")),
8722 Some(&"alice".to_string())
8723 );
8724
8725 let mut list_req = ListTablesRequest::new();
8726 list_req.id = Some(vec![]);
8727 assert_eq!(
8728 namespace
8729 .list_tables(list_req.clone())
8730 .await
8731 .unwrap()
8732 .tables,
8733 vec!["test_table".to_string()]
8734 );
8735 list_req.include_declared = Some(false);
8736 assert!(
8737 namespace
8738 .list_tables(list_req)
8739 .await
8740 .unwrap()
8741 .tables
8742 .is_empty()
8743 );
8744 }
8745
8746 #[tokio::test]
8747 async fn test_declare_table_when_table_exists() {
8748 use lance_namespace::models::DeclareTableRequest;
8749
8750 let temp_dir = TempStdDir::default();
8751 let temp_path = temp_dir.to_str().unwrap();
8752
8753 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8754 .manifest_enabled(false)
8755 .build()
8756 .await
8757 .unwrap();
8758
8759 let schema = create_test_schema();
8761 let ipc_data = create_test_ipc_data(&schema);
8762 let mut create_req = CreateTableRequest::new();
8763 create_req.id = Some(vec!["test_table".to_string()]);
8764 namespace
8765 .create_table(create_req, bytes::Bytes::from(ipc_data))
8766 .await
8767 .unwrap();
8768
8769 let mut declare_req = DeclareTableRequest::new();
8771 declare_req.id = Some(vec!["test_table".to_string()]);
8772 let result = namespace.declare_table(declare_req).await;
8773 assert!(result.is_err());
8774 }
8775
8776 #[tokio::test]
8781 async fn test_deregister_table_v1_mode() {
8782 use lance_namespace::models::{DeregisterTableRequest, TableExistsRequest};
8783
8784 let temp_dir = TempStdDir::default();
8785 let temp_path = temp_dir.to_str().unwrap();
8786
8787 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8789 .manifest_enabled(false)
8790 .dir_listing_enabled(true)
8791 .build()
8792 .await
8793 .unwrap();
8794
8795 let schema = create_test_schema();
8797 let ipc_data = create_test_ipc_data(&schema);
8798 let mut create_req = CreateTableRequest::new();
8799 create_req.id = Some(vec!["test_table".to_string()]);
8800 namespace
8801 .create_table(create_req, bytes::Bytes::from(ipc_data))
8802 .await
8803 .unwrap();
8804
8805 let mut exists_req = TableExistsRequest::new();
8807 exists_req.id = Some(vec!["test_table".to_string()]);
8808 assert!(namespace.table_exists(exists_req.clone()).await.is_ok());
8809
8810 let mut deregister_req = DeregisterTableRequest::new();
8812 deregister_req.id = Some(vec!["test_table".to_string()]);
8813 let response = namespace.deregister_table(deregister_req).await.unwrap();
8814
8815 assert!(response.location.is_some());
8817 let location = response.location.as_ref().unwrap();
8818 assert!(location.contains("test_table"));
8819
8820 let result = namespace.table_exists(exists_req).await;
8822 assert!(result.is_err());
8823 assert!(result.unwrap_err().to_string().contains("deregistered"));
8824
8825 let dataset = Dataset::open(location).await;
8827 assert!(dataset.is_ok(), "Physical table data should still exist");
8828 }
8829
8830 #[tokio::test]
8831 async fn test_deregister_table_v1_already_deregistered() {
8832 use lance_namespace::models::DeregisterTableRequest;
8833
8834 let temp_dir = TempStdDir::default();
8835 let temp_path = temp_dir.to_str().unwrap();
8836
8837 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8838 .manifest_enabled(false)
8839 .dir_listing_enabled(true)
8840 .build()
8841 .await
8842 .unwrap();
8843
8844 let schema = create_test_schema();
8846 let ipc_data = create_test_ipc_data(&schema);
8847 let mut create_req = CreateTableRequest::new();
8848 create_req.id = Some(vec!["test_table".to_string()]);
8849 namespace
8850 .create_table(create_req, bytes::Bytes::from(ipc_data))
8851 .await
8852 .unwrap();
8853
8854 let mut deregister_req = DeregisterTableRequest::new();
8856 deregister_req.id = Some(vec!["test_table".to_string()]);
8857 namespace
8858 .deregister_table(deregister_req.clone())
8859 .await
8860 .unwrap();
8861
8862 let result = namespace.deregister_table(deregister_req).await;
8864 assert!(result.is_err());
8865 assert!(
8866 result
8867 .unwrap_err()
8868 .to_string()
8869 .contains("already deregistered")
8870 );
8871 }
8872
8873 #[tokio::test]
8878 async fn test_list_tables_skips_deregistered_v1() {
8879 use lance_namespace::models::DeregisterTableRequest;
8880
8881 let temp_dir = TempStdDir::default();
8882 let temp_path = temp_dir.to_str().unwrap();
8883
8884 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8885 .manifest_enabled(false)
8886 .dir_listing_enabled(true)
8887 .build()
8888 .await
8889 .unwrap();
8890
8891 let schema = create_test_schema();
8893 let ipc_data = create_test_ipc_data(&schema);
8894
8895 let mut create_req1 = CreateTableRequest::new();
8896 create_req1.id = Some(vec!["table1".to_string()]);
8897 namespace
8898 .create_table(create_req1, bytes::Bytes::from(ipc_data.clone()))
8899 .await
8900 .unwrap();
8901
8902 let mut create_req2 = CreateTableRequest::new();
8903 create_req2.id = Some(vec!["table2".to_string()]);
8904 namespace
8905 .create_table(create_req2, bytes::Bytes::from(ipc_data))
8906 .await
8907 .unwrap();
8908
8909 let mut list_req = ListTablesRequest::new();
8911 list_req.id = Some(vec![]);
8912 let list_response = namespace.list_tables(list_req.clone()).await.unwrap();
8913 assert_eq!(list_response.tables.len(), 2);
8914
8915 let mut deregister_req = DeregisterTableRequest::new();
8917 deregister_req.id = Some(vec!["table1".to_string()]);
8918 namespace.deregister_table(deregister_req).await.unwrap();
8919
8920 let list_response = namespace.list_tables(list_req).await.unwrap();
8922 assert_eq!(list_response.tables.len(), 1);
8923 assert!(list_response.tables.contains(&"table2".to_string()));
8924 assert!(!list_response.tables.contains(&"table1".to_string()));
8925 }
8926
8927 #[tokio::test]
8932 async fn test_describe_table_fails_for_deregistered_v1() {
8933 use lance_namespace::models::{DeregisterTableRequest, DescribeTableRequest};
8934
8935 let temp_dir = TempStdDir::default();
8936 let temp_path = temp_dir.to_str().unwrap();
8937
8938 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8939 .manifest_enabled(false)
8940 .dir_listing_enabled(true)
8941 .build()
8942 .await
8943 .unwrap();
8944
8945 let schema = create_test_schema();
8947 let ipc_data = create_test_ipc_data(&schema);
8948 let mut create_req = CreateTableRequest::new();
8949 create_req.id = Some(vec!["test_table".to_string()]);
8950 namespace
8951 .create_table(create_req, bytes::Bytes::from(ipc_data))
8952 .await
8953 .unwrap();
8954
8955 let mut describe_req = DescribeTableRequest::new();
8957 describe_req.id = Some(vec!["test_table".to_string()]);
8958 assert!(namespace.describe_table(describe_req.clone()).await.is_ok());
8959
8960 let mut deregister_req = DeregisterTableRequest::new();
8962 deregister_req.id = Some(vec!["test_table".to_string()]);
8963 namespace.deregister_table(deregister_req).await.unwrap();
8964
8965 let result = namespace.describe_table(describe_req).await;
8967 assert!(result.is_err());
8968 let err = result.unwrap_err();
8969 assert!(matches!(err, Error::Namespace { .. }));
8970 let err_msg = err.to_string();
8971 assert!(err_msg.contains("deregistered"));
8972 assert!(err_msg.contains("table id 'test_table'"));
8973 }
8974
8975 #[tokio::test]
8976 async fn test_table_exists_fails_for_deregistered_v1() {
8977 use lance_namespace::models::{DeregisterTableRequest, TableExistsRequest};
8978
8979 let temp_dir = TempStdDir::default();
8980 let temp_path = temp_dir.to_str().unwrap();
8981
8982 let namespace = DirectoryNamespaceBuilder::new(temp_path)
8983 .manifest_enabled(false)
8984 .dir_listing_enabled(true)
8985 .build()
8986 .await
8987 .unwrap();
8988
8989 let schema = create_test_schema();
8991 let ipc_data = create_test_ipc_data(&schema);
8992 let mut create_req = CreateTableRequest::new();
8993 create_req.id = Some(vec!["test_table".to_string()]);
8994 namespace
8995 .create_table(create_req, bytes::Bytes::from(ipc_data))
8996 .await
8997 .unwrap();
8998
8999 let mut exists_req = TableExistsRequest::new();
9001 exists_req.id = Some(vec!["test_table".to_string()]);
9002 assert!(namespace.table_exists(exists_req.clone()).await.is_ok());
9003
9004 let mut deregister_req = DeregisterTableRequest::new();
9006 deregister_req.id = Some(vec!["test_table".to_string()]);
9007 namespace.deregister_table(deregister_req).await.unwrap();
9008
9009 let result = namespace.table_exists(exists_req).await;
9011 assert!(result.is_err());
9012 let err = result.unwrap_err();
9013 assert!(matches!(err, Error::Namespace { .. }));
9014 let err_msg = err.to_string();
9015 assert!(err_msg.contains("deregistered"));
9016 assert!(err_msg.contains("table id 'test_table'"));
9017 }
9018
9019 #[tokio::test]
9020 async fn test_atomic_table_status_check() {
9021 let temp_dir = TempStdDir::default();
9025 let temp_path = temp_dir.to_str().unwrap();
9026
9027 let namespace = DirectoryNamespaceBuilder::new(temp_path)
9028 .manifest_enabled(false)
9029 .dir_listing_enabled(true)
9030 .build()
9031 .await
9032 .unwrap();
9033
9034 let schema = create_test_schema();
9036 let ipc_data = create_test_ipc_data(&schema);
9037 let mut create_req = CreateTableRequest::new();
9038 create_req.id = Some(vec!["test_table".to_string()]);
9039 namespace
9040 .create_table(create_req, bytes::Bytes::from(ipc_data))
9041 .await
9042 .unwrap();
9043
9044 let status = namespace.check_table_status("test_table").await;
9046 assert!(status.exists);
9047 assert!(!status.is_deregistered);
9048 assert!(!status.has_reserved_file);
9049 }
9050
9051 #[tokio::test]
9052 async fn test_table_version_tracking_enabled_managed_versioning() {
9053 use lance_namespace::models::DescribeTableRequest;
9054
9055 let temp_dir = TempStdDir::default();
9056 let temp_path = temp_dir.to_str().unwrap();
9057
9058 let namespace = DirectoryNamespaceBuilder::new(temp_path)
9060 .table_version_tracking_enabled(true)
9061 .build()
9062 .await
9063 .unwrap();
9064
9065 let schema = create_test_schema();
9067 let ipc_data = create_test_ipc_data(&schema);
9068 let mut create_req = CreateTableRequest::new();
9069 create_req.id = Some(vec!["test_table".to_string()]);
9070 namespace
9071 .create_table(create_req, bytes::Bytes::from(ipc_data))
9072 .await
9073 .unwrap();
9074
9075 let mut describe_req = DescribeTableRequest::new();
9077 describe_req.id = Some(vec!["test_table".to_string()]);
9078 let describe_resp = namespace.describe_table(describe_req).await.unwrap();
9079
9080 assert_eq!(
9082 describe_resp.managed_versioning,
9083 Some(true),
9084 "managed_versioning should be true when table_version_tracking_enabled=true"
9085 );
9086 }
9087
9088 #[tokio::test]
9089 async fn test_table_version_tracking_disabled_no_managed_versioning() {
9090 use lance_namespace::models::DescribeTableRequest;
9091
9092 let temp_dir = TempStdDir::default();
9093 let temp_path = temp_dir.to_str().unwrap();
9094
9095 let namespace = DirectoryNamespaceBuilder::new(temp_path)
9097 .table_version_tracking_enabled(false)
9098 .build()
9099 .await
9100 .unwrap();
9101
9102 let schema = create_test_schema();
9104 let ipc_data = create_test_ipc_data(&schema);
9105 let mut create_req = CreateTableRequest::new();
9106 create_req.id = Some(vec!["test_table".to_string()]);
9107 namespace
9108 .create_table(create_req, bytes::Bytes::from(ipc_data))
9109 .await
9110 .unwrap();
9111
9112 let mut describe_req = DescribeTableRequest::new();
9114 describe_req.id = Some(vec!["test_table".to_string()]);
9115 let describe_resp = namespace.describe_table(describe_req).await.unwrap();
9116
9117 assert!(
9119 describe_resp.managed_versioning.is_none(),
9120 "managed_versioning should be None when table_version_tracking_enabled=false, got: {:?}",
9121 describe_resp.managed_versioning
9122 );
9123 }
9124
9125 #[tokio::test]
9126 async fn test_list_table_versions() {
9127 use arrow::array::{Int32Array, RecordBatchIterator};
9128 use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
9129 use arrow::record_batch::RecordBatch;
9130 use lance::dataset::{Dataset, WriteMode, WriteParams};
9131 use lance_namespace::models::{CreateNamespaceRequest, ListTableVersionsRequest};
9132
9133 let temp_dir = TempStrDir::default();
9134 let temp_path: &str = &temp_dir;
9135
9136 let namespace: Arc<dyn LanceNamespace> = Arc::new(
9137 DirectoryNamespaceBuilder::new(temp_path)
9138 .table_version_tracking_enabled(true)
9139 .build()
9140 .await
9141 .unwrap(),
9142 );
9143
9144 let mut create_ns_req = CreateNamespaceRequest::new();
9146 create_ns_req.id = Some(vec!["workspace".to_string()]);
9147 namespace.create_namespace(create_ns_req).await.unwrap();
9148
9149 let table_id = vec!["workspace".to_string(), "test_table".to_string()];
9151 let arrow_schema = Arc::new(ArrowSchema::new(vec![Field::new(
9152 "id",
9153 DataType::Int32,
9154 false,
9155 )]));
9156 let batch = RecordBatch::try_new(
9157 arrow_schema.clone(),
9158 vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
9159 )
9160 .unwrap();
9161 let batches = RecordBatchIterator::new(vec![Ok(batch.clone())], arrow_schema.clone());
9162 let write_params = WriteParams {
9163 mode: WriteMode::Create,
9164 ..Default::default()
9165 };
9166 let mut dataset = Dataset::write_into_namespace(
9167 batches,
9168 namespace.clone(),
9169 table_id.clone(),
9170 Some(write_params),
9171 )
9172 .await
9173 .unwrap();
9174
9175 let batch2 = RecordBatch::try_new(
9177 arrow_schema.clone(),
9178 vec![Arc::new(Int32Array::from(vec![100, 200]))],
9179 )
9180 .unwrap();
9181 let batches = RecordBatchIterator::new(vec![Ok(batch2)], arrow_schema.clone());
9182 dataset.append(batches, None).await.unwrap();
9183
9184 let batch3 = RecordBatch::try_new(
9186 arrow_schema.clone(),
9187 vec![Arc::new(Int32Array::from(vec![300, 400]))],
9188 )
9189 .unwrap();
9190 let batches = RecordBatchIterator::new(vec![Ok(batch3)], arrow_schema);
9191 dataset.append(batches, None).await.unwrap();
9192
9193 let mut list_req = ListTableVersionsRequest::new();
9195 list_req.id = Some(table_id.clone());
9196 let list_resp = namespace.list_table_versions(list_req).await.unwrap();
9197
9198 assert_eq!(
9199 list_resp.versions.len(),
9200 3,
9201 "Should have 3 versions, got: {:?}",
9202 list_resp.versions
9203 );
9204
9205 for expected_version in 1..=3 {
9207 let version = list_resp
9208 .versions
9209 .iter()
9210 .find(|v| v.version == expected_version)
9211 .unwrap_or_else(|| panic!("Expected version {}", expected_version));
9212
9213 assert!(
9214 !version.manifest_path.is_empty(),
9215 "manifest_path should be set for version {}",
9216 expected_version
9217 );
9218 assert!(
9219 version.manifest_path.contains(".manifest"),
9220 "manifest_path should contain .manifest for version {}",
9221 expected_version
9222 );
9223 assert!(
9224 version.manifest_size.is_some(),
9225 "manifest_size should be set for version {}",
9226 expected_version
9227 );
9228 assert!(
9229 version.manifest_size.unwrap() > 0,
9230 "manifest_size should be > 0 for version {}",
9231 expected_version
9232 );
9233 assert!(
9234 version.timestamp_millis.is_some(),
9235 "timestamp_millis should be set for version {}",
9236 expected_version
9237 );
9238 }
9239 }
9240
9241 #[tokio::test]
9242 async fn test_describe_table_version() {
9243 use arrow::array::{Int32Array, RecordBatchIterator};
9244 use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
9245 use arrow::record_batch::RecordBatch;
9246 use lance::dataset::{Dataset, WriteMode, WriteParams};
9247 use lance_namespace::models::{CreateNamespaceRequest, DescribeTableVersionRequest};
9248
9249 let temp_dir = TempStrDir::default();
9250 let temp_path: &str = &temp_dir;
9251
9252 let namespace: Arc<dyn LanceNamespace> = Arc::new(
9253 DirectoryNamespaceBuilder::new(temp_path)
9254 .table_version_tracking_enabled(true)
9255 .build()
9256 .await
9257 .unwrap(),
9258 );
9259
9260 let mut create_ns_req = CreateNamespaceRequest::new();
9262 create_ns_req.id = Some(vec!["workspace".to_string()]);
9263 namespace.create_namespace(create_ns_req).await.unwrap();
9264
9265 let table_id = vec!["workspace".to_string(), "test_table".to_string()];
9267 let arrow_schema = Arc::new(ArrowSchema::new(vec![Field::new(
9268 "id",
9269 DataType::Int32,
9270 false,
9271 )]));
9272 let batch = RecordBatch::try_new(
9273 arrow_schema.clone(),
9274 vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
9275 )
9276 .unwrap();
9277 let batches = RecordBatchIterator::new(vec![Ok(batch)], arrow_schema.clone());
9278 let write_params = WriteParams {
9279 mode: WriteMode::Create,
9280 ..Default::default()
9281 };
9282 let mut dataset = Dataset::write_into_namespace(
9283 batches,
9284 namespace.clone(),
9285 table_id.clone(),
9286 Some(write_params),
9287 )
9288 .await
9289 .unwrap();
9290
9291 let batch2 = RecordBatch::try_new(
9293 arrow_schema.clone(),
9294 vec![Arc::new(Int32Array::from(vec![100, 200]))],
9295 )
9296 .unwrap();
9297 let batches = RecordBatchIterator::new(vec![Ok(batch2)], arrow_schema);
9298 dataset.append(batches, None).await.unwrap();
9299
9300 let mut describe_req = DescribeTableVersionRequest::new();
9302 describe_req.id = Some(table_id.clone());
9303 describe_req.version = Some(1);
9304 let describe_resp = namespace
9305 .describe_table_version(describe_req)
9306 .await
9307 .unwrap();
9308
9309 let version = &describe_resp.version;
9310 assert_eq!(version.version, 1);
9311 assert!(version.timestamp_millis.is_some());
9312 assert!(
9313 !version.manifest_path.is_empty(),
9314 "manifest_path should be set"
9315 );
9316 assert!(
9317 version.manifest_path.contains(".manifest"),
9318 "manifest_path should contain .manifest"
9319 );
9320 assert!(
9321 version.manifest_size.is_some(),
9322 "manifest_size should be set"
9323 );
9324 assert!(
9325 version.manifest_size.unwrap() > 0,
9326 "manifest_size should be > 0"
9327 );
9328
9329 let mut describe_req = DescribeTableVersionRequest::new();
9331 describe_req.id = Some(table_id.clone());
9332 describe_req.version = Some(2);
9333 let describe_resp = namespace
9334 .describe_table_version(describe_req)
9335 .await
9336 .unwrap();
9337
9338 let version = &describe_resp.version;
9339 assert_eq!(version.version, 2);
9340 assert!(version.timestamp_millis.is_some());
9341 assert!(
9342 !version.manifest_path.is_empty(),
9343 "manifest_path should be set"
9344 );
9345 assert!(
9346 version.manifest_size.is_some(),
9347 "manifest_size should be set"
9348 );
9349 assert!(
9350 version.manifest_size.unwrap() > 0,
9351 "manifest_size should be > 0"
9352 );
9353 }
9354
9355 #[tokio::test]
9356 async fn test_describe_table_version_latest() {
9357 use arrow::array::{Int32Array, RecordBatchIterator};
9358 use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
9359 use arrow::record_batch::RecordBatch;
9360 use lance::dataset::{Dataset, WriteMode, WriteParams};
9361 use lance_namespace::models::{CreateNamespaceRequest, DescribeTableVersionRequest};
9362
9363 let temp_dir = TempStrDir::default();
9364 let temp_path: &str = &temp_dir;
9365
9366 let namespace: Arc<dyn LanceNamespace> = Arc::new(
9367 DirectoryNamespaceBuilder::new(temp_path)
9368 .table_version_tracking_enabled(true)
9369 .build()
9370 .await
9371 .unwrap(),
9372 );
9373
9374 let mut create_ns_req = CreateNamespaceRequest::new();
9376 create_ns_req.id = Some(vec!["workspace".to_string()]);
9377 namespace.create_namespace(create_ns_req).await.unwrap();
9378
9379 let table_id = vec!["workspace".to_string(), "test_table".to_string()];
9381 let arrow_schema = Arc::new(ArrowSchema::new(vec![Field::new(
9382 "id",
9383 DataType::Int32,
9384 false,
9385 )]));
9386 let batch = RecordBatch::try_new(
9387 arrow_schema.clone(),
9388 vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
9389 )
9390 .unwrap();
9391 let batches = RecordBatchIterator::new(vec![Ok(batch)], arrow_schema.clone());
9392 let write_params = WriteParams {
9393 mode: WriteMode::Create,
9394 ..Default::default()
9395 };
9396 let mut dataset = Dataset::write_into_namespace(
9397 batches,
9398 namespace.clone(),
9399 table_id.clone(),
9400 Some(write_params),
9401 )
9402 .await
9403 .unwrap();
9404
9405 let batch2 = RecordBatch::try_new(
9407 arrow_schema.clone(),
9408 vec![Arc::new(Int32Array::from(vec![100, 200]))],
9409 )
9410 .unwrap();
9411 let batches = RecordBatchIterator::new(vec![Ok(batch2)], arrow_schema.clone());
9412 dataset.append(batches, None).await.unwrap();
9413
9414 let batch3 = RecordBatch::try_new(
9416 arrow_schema.clone(),
9417 vec![Arc::new(Int32Array::from(vec![300, 400]))],
9418 )
9419 .unwrap();
9420 let batches = RecordBatchIterator::new(vec![Ok(batch3)], arrow_schema);
9421 dataset.append(batches, None).await.unwrap();
9422
9423 let mut describe_req = DescribeTableVersionRequest::new();
9425 describe_req.id = Some(table_id.clone());
9426 describe_req.version = None;
9427 let describe_resp = namespace
9428 .describe_table_version(describe_req)
9429 .await
9430 .unwrap();
9431
9432 assert_eq!(describe_resp.version.version, 3);
9434 }
9435
9436 #[tokio::test]
9437 async fn test_create_table_version() {
9438 use futures::TryStreamExt;
9439 use lance::dataset::builder::DatasetBuilder;
9440 use lance_namespace::models::CreateTableVersionRequest;
9441
9442 let temp_dir = TempStrDir::default();
9443 let temp_path: &str = &temp_dir;
9444
9445 let namespace: Arc<dyn LanceNamespace> = Arc::new(
9446 DirectoryNamespaceBuilder::new(temp_path)
9447 .table_version_tracking_enabled(true)
9448 .build()
9449 .await
9450 .unwrap(),
9451 );
9452
9453 let schema = create_test_schema();
9455 let ipc_data = create_test_ipc_data(&schema);
9456 let mut create_req = CreateTableRequest::new();
9457 create_req.id = Some(vec!["test_table".to_string()]);
9458 namespace
9459 .create_table(create_req, bytes::Bytes::from(ipc_data))
9460 .await
9461 .unwrap();
9462
9463 let table_id = vec!["test_table".to_string()];
9465 let dataset = DatasetBuilder::from_namespace(namespace.clone(), table_id.clone())
9466 .await
9467 .unwrap()
9468 .load()
9469 .await
9470 .unwrap();
9471
9472 let versions_path = dataset.versions_dir();
9474 let manifest_metas: Vec<_> = dataset
9475 .object_store(None)
9476 .await
9477 .unwrap()
9478 .inner
9479 .list(Some(&versions_path))
9480 .try_collect()
9481 .await
9482 .unwrap();
9483
9484 let manifest_meta = manifest_metas
9485 .iter()
9486 .find(|m| {
9487 m.location
9488 .filename()
9489 .map(|f| f.ends_with(".manifest"))
9490 .unwrap_or(false)
9491 })
9492 .expect("No manifest file found");
9493
9494 let manifest_data = dataset
9496 .object_store(None)
9497 .await
9498 .unwrap()
9499 .inner
9500 .get(&manifest_meta.location)
9501 .await
9502 .unwrap()
9503 .bytes()
9504 .await
9505 .unwrap();
9506
9507 let staging_path = dataset.versions_dir().join("staging_manifest");
9509 dataset
9510 .object_store(None)
9511 .await
9512 .unwrap()
9513 .inner
9514 .put(&staging_path, manifest_data.into())
9515 .await
9516 .unwrap();
9517
9518 let mut create_version_req = CreateTableVersionRequest::new(2, staging_path.to_string());
9521 create_version_req.id = Some(table_id.clone());
9522 create_version_req.naming_scheme = Some("V2".to_string());
9523
9524 let result = namespace.create_table_version(create_version_req).await;
9525 assert!(
9526 result.is_ok(),
9527 "create_table_version should succeed: {:?}",
9528 result
9529 );
9530
9531 let response = result.unwrap();
9533 let version_info = response
9534 .version
9535 .expect("response should contain version info");
9536 let version_2_path = Path::parse(&version_info.manifest_path).unwrap();
9537 let head_result = dataset
9538 .object_store(None)
9539 .await
9540 .unwrap()
9541 .inner
9542 .head(&version_2_path)
9543 .await;
9544 assert!(
9545 head_result.is_ok(),
9546 "Version 2 manifest should exist at {}",
9547 version_2_path
9548 );
9549
9550 let staging_head_result = dataset
9552 .object_store(None)
9553 .await
9554 .unwrap()
9555 .inner
9556 .head(&staging_path)
9557 .await;
9558 assert!(
9559 staging_head_result.is_err(),
9560 "Staging manifest should have been deleted after create_table_version"
9561 );
9562 }
9563
9564 #[tokio::test]
9565 async fn test_create_table_version_conflict() {
9566 use futures::TryStreamExt;
9569 use lance::dataset::builder::DatasetBuilder;
9570 use lance_namespace::models::CreateTableVersionRequest;
9571
9572 let temp_dir = TempStrDir::default();
9573 let temp_path: &str = &temp_dir;
9574
9575 let namespace: Arc<dyn LanceNamespace> = Arc::new(
9576 DirectoryNamespaceBuilder::new(temp_path)
9577 .table_version_tracking_enabled(true)
9578 .build()
9579 .await
9580 .unwrap(),
9581 );
9582
9583 let schema = create_test_schema();
9585 let ipc_data = create_test_ipc_data(&schema);
9586 let mut create_req = CreateTableRequest::new();
9587 create_req.id = Some(vec!["test_table".to_string()]);
9588 namespace
9589 .create_table(create_req, bytes::Bytes::from(ipc_data))
9590 .await
9591 .unwrap();
9592
9593 let table_id = vec!["test_table".to_string()];
9595 let dataset = DatasetBuilder::from_namespace(namespace.clone(), table_id.clone())
9596 .await
9597 .unwrap()
9598 .load()
9599 .await
9600 .unwrap();
9601
9602 let versions_path = dataset.versions_dir();
9604 let manifest_metas: Vec<_> = dataset
9605 .object_store(None)
9606 .await
9607 .unwrap()
9608 .inner
9609 .list(Some(&versions_path))
9610 .try_collect()
9611 .await
9612 .unwrap();
9613
9614 let manifest_meta = manifest_metas
9615 .iter()
9616 .find(|m| {
9617 m.location
9618 .filename()
9619 .map(|f| f.ends_with(".manifest"))
9620 .unwrap_or(false)
9621 })
9622 .expect("No manifest file found");
9623
9624 let manifest_data = dataset
9626 .object_store(None)
9627 .await
9628 .unwrap()
9629 .inner
9630 .get(&manifest_meta.location)
9631 .await
9632 .unwrap()
9633 .bytes()
9634 .await
9635 .unwrap();
9636
9637 let staging_path = dataset.versions_dir().join("staging_manifest");
9639 dataset
9640 .object_store(None)
9641 .await
9642 .unwrap()
9643 .inner
9644 .put(&staging_path, manifest_data.into())
9645 .await
9646 .unwrap();
9647
9648 let mut create_version_req = CreateTableVersionRequest::new(2, staging_path.to_string());
9650 create_version_req.id = Some(table_id.clone());
9651 create_version_req.naming_scheme = Some("V2".to_string());
9652 let first_result = namespace.create_table_version(create_version_req).await;
9653 assert!(
9654 first_result.is_ok(),
9655 "First create_table_version for version 2 should succeed: {:?}",
9656 first_result
9657 );
9658
9659 let version_2_path = Path::parse(
9661 &first_result
9662 .unwrap()
9663 .version
9664 .expect("response should contain version info")
9665 .manifest_path,
9666 )
9667 .unwrap();
9668
9669 let mut create_version_req = CreateTableVersionRequest::new(2, staging_path.to_string());
9671 create_version_req.id = Some(table_id.clone());
9672 create_version_req.naming_scheme = Some("V2".to_string());
9673
9674 let result = namespace.create_table_version(create_version_req).await;
9675 assert!(
9676 result.is_err(),
9677 "create_table_version should fail for existing version"
9678 );
9679
9680 let head_result = dataset
9682 .object_store(None)
9683 .await
9684 .unwrap()
9685 .inner
9686 .head(&version_2_path)
9687 .await;
9688 assert!(
9689 head_result.is_ok(),
9690 "Version 2 manifest should still exist at {}",
9691 version_2_path
9692 );
9693 }
9694
9695 #[tokio::test]
9696 async fn test_create_table_version_table_not_found() {
9697 use lance_namespace::models::CreateTableVersionRequest;
9698
9699 let temp_dir = TempStdDir::default();
9700 let temp_path = temp_dir.to_str().unwrap();
9701
9702 let namespace = DirectoryNamespaceBuilder::new(temp_path)
9703 .table_version_tracking_enabled(true)
9704 .build()
9705 .await
9706 .unwrap();
9707
9708 let mut create_version_req =
9710 CreateTableVersionRequest::new(1, "/some/staging/path".to_string());
9711 create_version_req.id = Some(vec!["non_existent_table".to_string()]);
9712
9713 let result = namespace.create_table_version(create_version_req).await;
9714 assert!(
9715 result.is_err(),
9716 "create_table_version should fail for non-existent table"
9717 );
9718 let err_msg = result.unwrap_err().to_string();
9719 assert!(
9720 err_msg.contains("Table not found"),
9721 "Error should mention table not found, got: {}",
9722 err_msg
9723 );
9724 }
9725
9726 mod e2e_table_version_tracking {
9728 use super::*;
9729 use std::sync::atomic::{AtomicUsize, Ordering};
9730
9731 struct TrackingNamespace {
9733 inner: DirectoryNamespace,
9734 create_table_version_count: AtomicUsize,
9735 describe_table_version_count: AtomicUsize,
9736 list_table_versions_count: AtomicUsize,
9737 }
9738
9739 impl TrackingNamespace {
9740 fn new(inner: DirectoryNamespace) -> Self {
9741 Self {
9742 inner,
9743 create_table_version_count: AtomicUsize::new(0),
9744 describe_table_version_count: AtomicUsize::new(0),
9745 list_table_versions_count: AtomicUsize::new(0),
9746 }
9747 }
9748
9749 fn create_table_version_calls(&self) -> usize {
9750 self.create_table_version_count.load(Ordering::SeqCst)
9751 }
9752
9753 fn describe_table_version_calls(&self) -> usize {
9754 self.describe_table_version_count.load(Ordering::SeqCst)
9755 }
9756
9757 fn list_table_versions_calls(&self) -> usize {
9758 self.list_table_versions_count.load(Ordering::SeqCst)
9759 }
9760 }
9761
9762 impl std::fmt::Debug for TrackingNamespace {
9763 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
9764 f.debug_struct("TrackingNamespace")
9765 .field(
9766 "create_table_version_calls",
9767 &self.create_table_version_calls(),
9768 )
9769 .finish()
9770 }
9771 }
9772
9773 #[async_trait]
9774 impl LanceNamespace for TrackingNamespace {
9775 async fn create_namespace(
9776 &self,
9777 request: CreateNamespaceRequest,
9778 ) -> Result<CreateNamespaceResponse> {
9779 self.inner.create_namespace(request).await
9780 }
9781
9782 async fn describe_namespace(
9783 &self,
9784 request: DescribeNamespaceRequest,
9785 ) -> Result<DescribeNamespaceResponse> {
9786 self.inner.describe_namespace(request).await
9787 }
9788
9789 async fn namespace_exists(&self, request: NamespaceExistsRequest) -> Result<()> {
9790 self.inner.namespace_exists(request).await
9791 }
9792
9793 async fn list_namespaces(
9794 &self,
9795 request: ListNamespacesRequest,
9796 ) -> Result<ListNamespacesResponse> {
9797 self.inner.list_namespaces(request).await
9798 }
9799
9800 async fn drop_namespace(
9801 &self,
9802 request: DropNamespaceRequest,
9803 ) -> Result<DropNamespaceResponse> {
9804 self.inner.drop_namespace(request).await
9805 }
9806
9807 async fn list_tables(&self, request: ListTablesRequest) -> Result<ListTablesResponse> {
9808 self.inner.list_tables(request).await
9809 }
9810
9811 async fn describe_table(
9812 &self,
9813 request: DescribeTableRequest,
9814 ) -> Result<DescribeTableResponse> {
9815 self.inner.describe_table(request).await
9816 }
9817
9818 async fn table_exists(&self, request: TableExistsRequest) -> Result<()> {
9819 self.inner.table_exists(request).await
9820 }
9821
9822 async fn drop_table(&self, request: DropTableRequest) -> Result<DropTableResponse> {
9823 self.inner.drop_table(request).await
9824 }
9825
9826 async fn create_table(
9827 &self,
9828 request: CreateTableRequest,
9829 request_data: Bytes,
9830 ) -> Result<CreateTableResponse> {
9831 self.inner.create_table(request, request_data).await
9832 }
9833
9834 async fn declare_table(
9835 &self,
9836 request: DeclareTableRequest,
9837 ) -> Result<DeclareTableResponse> {
9838 self.inner.declare_table(request).await
9839 }
9840
9841 async fn list_table_versions(
9842 &self,
9843 request: ListTableVersionsRequest,
9844 ) -> Result<ListTableVersionsResponse> {
9845 self.list_table_versions_count
9846 .fetch_add(1, Ordering::SeqCst);
9847 self.inner.list_table_versions(request).await
9848 }
9849
9850 async fn create_table_version(
9851 &self,
9852 request: CreateTableVersionRequest,
9853 ) -> Result<CreateTableVersionResponse> {
9854 self.create_table_version_count
9855 .fetch_add(1, Ordering::SeqCst);
9856 self.inner.create_table_version(request).await
9857 }
9858
9859 async fn describe_table_version(
9860 &self,
9861 request: DescribeTableVersionRequest,
9862 ) -> Result<DescribeTableVersionResponse> {
9863 self.describe_table_version_count
9864 .fetch_add(1, Ordering::SeqCst);
9865 self.inner.describe_table_version(request).await
9866 }
9867
9868 async fn batch_delete_table_versions(
9869 &self,
9870 request: BatchDeleteTableVersionsRequest,
9871 ) -> Result<BatchDeleteTableVersionsResponse> {
9872 self.inner.batch_delete_table_versions(request).await
9873 }
9874
9875 fn namespace_id(&self) -> String {
9876 self.inner.namespace_id()
9877 }
9878 }
9879
9880 #[tokio::test]
9881 async fn test_describe_table_returns_managed_versioning() {
9882 use lance_namespace::models::{CreateNamespaceRequest, DescribeTableRequest};
9883
9884 let temp_dir = TempStdDir::default();
9885 let temp_path = temp_dir.to_str().unwrap();
9886
9887 let ns = DirectoryNamespaceBuilder::new(temp_path)
9889 .table_version_tracking_enabled(true)
9890 .manifest_enabled(true)
9891 .build()
9892 .await
9893 .unwrap();
9894
9895 let mut create_ns_req = CreateNamespaceRequest::new();
9897 create_ns_req.id = Some(vec!["workspace".to_string()]);
9898 ns.create_namespace(create_ns_req).await.unwrap();
9899
9900 let schema = create_test_schema();
9902 let ipc_data = create_test_ipc_data(&schema);
9903 let mut create_req = CreateTableRequest::new();
9904 create_req.id = Some(vec!["workspace".to_string(), "test_table".to_string()]);
9905 ns.create_table(create_req, bytes::Bytes::from(ipc_data))
9906 .await
9907 .unwrap();
9908
9909 let mut describe_req = DescribeTableRequest::new();
9911 describe_req.id = Some(vec!["workspace".to_string(), "test_table".to_string()]);
9912 let describe_resp = ns.describe_table(describe_req).await.unwrap();
9913
9914 assert_eq!(
9916 describe_resp.managed_versioning,
9917 Some(true),
9918 "managed_versioning should be true when table_version_tracking_enabled=true"
9919 );
9920 }
9921
9922 #[tokio::test]
9923 async fn test_external_manifest_store_invokes_namespace_apis() {
9924 use arrow::array::{Int32Array, StringArray};
9925 use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
9926 use arrow::record_batch::RecordBatch;
9927 use lance::Dataset;
9928 use lance::dataset::builder::DatasetBuilder;
9929 use lance::dataset::{WriteMode, WriteParams};
9930 use lance_namespace::models::CreateNamespaceRequest;
9931
9932 let temp_dir = TempStdDir::default();
9933 let temp_path = temp_dir.to_str().unwrap();
9934
9935 let inner_ns = DirectoryNamespaceBuilder::new(temp_path)
9937 .table_version_tracking_enabled(true)
9938 .manifest_enabled(true)
9939 .build()
9940 .await
9941 .unwrap();
9942
9943 let tracking_ns = Arc::new(TrackingNamespace::new(inner_ns));
9944 let ns: Arc<dyn LanceNamespace> = tracking_ns.clone();
9945
9946 let mut create_ns_req = CreateNamespaceRequest::new();
9948 create_ns_req.id = Some(vec!["workspace".to_string()]);
9949 ns.create_namespace(create_ns_req).await.unwrap();
9950
9951 let table_id = vec!["workspace".to_string(), "test_table".to_string()];
9953
9954 let arrow_schema = Arc::new(ArrowSchema::new(vec![
9956 Field::new("id", DataType::Int32, false),
9957 Field::new("name", DataType::Utf8, true),
9958 ]));
9959 let batch = RecordBatch::try_new(
9960 arrow_schema.clone(),
9961 vec![
9962 Arc::new(Int32Array::from(vec![1, 2, 3])),
9963 Arc::new(StringArray::from(vec!["a", "b", "c"])),
9964 ],
9965 )
9966 .unwrap();
9967
9968 let batches = RecordBatchIterator::new(vec![Ok(batch.clone())], arrow_schema.clone());
9970 let write_params = WriteParams {
9971 mode: WriteMode::Create,
9972 ..Default::default()
9973 };
9974 let mut dataset = Dataset::write_into_namespace(
9975 batches,
9976 ns.clone(),
9977 table_id.clone(),
9978 Some(write_params),
9979 )
9980 .await
9981 .unwrap();
9982 assert_eq!(dataset.version().version, 1);
9983
9984 assert_eq!(
9986 tracking_ns.create_table_version_calls(),
9987 1,
9988 "create_table_version should have been called once during initial write_into_namespace"
9989 );
9990
9991 let append_batch = RecordBatch::try_new(
9993 arrow_schema.clone(),
9994 vec![
9995 Arc::new(Int32Array::from(vec![4, 5, 6])),
9996 Arc::new(StringArray::from(vec!["d", "e", "f"])),
9997 ],
9998 )
9999 .unwrap();
10000 let append_batches = RecordBatchIterator::new(vec![Ok(append_batch)], arrow_schema);
10001 dataset.append(append_batches, None).await.unwrap();
10002
10003 assert_eq!(
10004 tracking_ns.create_table_version_calls(),
10005 2,
10006 "create_table_version should have been called twice (once for create, once for append)"
10007 );
10008
10009 let initial_list_calls = tracking_ns.list_table_versions_calls();
10011 let latest_dataset = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
10012 .await
10013 .unwrap()
10014 .load()
10015 .await
10016 .unwrap();
10017 assert_eq!(latest_dataset.version().version, 2);
10018 assert_eq!(
10019 tracking_ns.list_table_versions_calls(),
10020 initial_list_calls + 1,
10021 "list_table_versions should have been called exactly once during checkout_latest"
10022 );
10023
10024 let initial_describe_calls = tracking_ns.describe_table_version_calls();
10026 let v1_dataset = DatasetBuilder::from_namespace(ns.clone(), table_id.clone())
10027 .await
10028 .unwrap()
10029 .with_version(1)
10030 .load()
10031 .await
10032 .unwrap();
10033 assert_eq!(v1_dataset.version().version, 1);
10034 assert_eq!(
10035 tracking_ns.describe_table_version_calls(),
10036 initial_describe_calls + 1,
10037 "describe_table_version should have been called exactly once during checkout to version 1"
10038 );
10039 }
10040
10041 #[tokio::test]
10042 async fn test_dataset_commit_with_external_manifest_store() {
10043 use arrow::array::{Int32Array, StringArray};
10044 use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
10045 use arrow::record_batch::RecordBatch;
10046 use futures::TryStreamExt;
10047 use lance::dataset::{Dataset, WriteMode, WriteParams};
10048 use lance_namespace::models::CreateNamespaceRequest;
10049 use lance_table::io::commit::ManifestNamingScheme;
10050
10051 let temp_dir = TempStdDir::default();
10052 let temp_path = temp_dir.to_str().unwrap();
10053
10054 let inner_ns = DirectoryNamespaceBuilder::new(temp_path)
10056 .table_version_tracking_enabled(true)
10057 .manifest_enabled(true)
10058 .build()
10059 .await
10060 .unwrap();
10061
10062 let tracking_ns: Arc<dyn LanceNamespace> = Arc::new(TrackingNamespace::new(inner_ns));
10063
10064 let mut create_ns_req = CreateNamespaceRequest::new();
10066 create_ns_req.id = Some(vec!["workspace".to_string()]);
10067 tracking_ns.create_namespace(create_ns_req).await.unwrap();
10068
10069 let table_id = vec!["workspace".to_string(), "test_table".to_string()];
10071 let arrow_schema = Arc::new(ArrowSchema::new(vec![
10072 Field::new("id", DataType::Int32, false),
10073 Field::new("name", DataType::Utf8, true),
10074 ]));
10075 let batch = RecordBatch::try_new(
10076 arrow_schema.clone(),
10077 vec![
10078 Arc::new(Int32Array::from(vec![1, 2, 3])),
10079 Arc::new(StringArray::from(vec!["a", "b", "c"])),
10080 ],
10081 )
10082 .unwrap();
10083 let batches = RecordBatchIterator::new(vec![Ok(batch)], arrow_schema.clone());
10084 let write_params = WriteParams {
10085 mode: WriteMode::Create,
10086 ..Default::default()
10087 };
10088 let dataset = Dataset::write_into_namespace(
10089 batches,
10090 tracking_ns.clone(),
10091 table_id.clone(),
10092 Some(write_params),
10093 )
10094 .await
10095 .unwrap();
10096 assert_eq!(dataset.version().version, 1);
10097
10098 let batch2 = RecordBatch::try_new(
10100 arrow_schema.clone(),
10101 vec![
10102 Arc::new(Int32Array::from(vec![4, 5, 6])),
10103 Arc::new(StringArray::from(vec!["d", "e", "f"])),
10104 ],
10105 )
10106 .unwrap();
10107 let batches = RecordBatchIterator::new(vec![Ok(batch2)], arrow_schema);
10108 let write_params = WriteParams {
10109 mode: WriteMode::Append,
10110 ..Default::default()
10111 };
10112 Dataset::write_into_namespace(
10113 batches,
10114 tracking_ns.clone(),
10115 table_id.clone(),
10116 Some(write_params),
10117 )
10118 .await
10119 .unwrap();
10120
10121 let manifest_metas: Vec<_> = dataset
10124 .object_store(None)
10125 .await
10126 .unwrap()
10127 .inner
10128 .list(Some(&dataset.versions_dir()))
10129 .try_collect()
10130 .await
10131 .unwrap();
10132 let version_2_found = manifest_metas.iter().any(|m| {
10133 m.location
10134 .filename()
10135 .map(|f| {
10136 f.ends_with(".manifest")
10137 && ManifestNamingScheme::V2.parse_version(f) == Some(2)
10138 })
10139 .unwrap_or(false)
10140 });
10141 assert!(
10142 version_2_found,
10143 "Version 2 manifest should exist in versions directory"
10144 );
10145 }
10146
10147 async fn create_ns_with_table() -> (DirectoryNamespace, TempStdDir, Vec<String>) {
10149 use arrow::array::{Int32Array, StringArray};
10150 use arrow::ipc::writer::StreamWriter;
10151
10152 let (namespace, temp_dir) = create_test_namespace().await;
10153
10154 let schema = create_test_schema();
10155 let arrow_schema = convert_json_arrow_schema(&schema).unwrap();
10156 let arrow_schema = Arc::new(arrow_schema);
10157
10158 let id_array = Int32Array::from(vec![1, 2, 3]);
10159 let name_array = StringArray::from(vec!["Alice", "Bob", "Charlie"]);
10160 let batch = arrow::record_batch::RecordBatch::try_new(
10161 arrow_schema.clone(),
10162 vec![Arc::new(id_array), Arc::new(name_array)],
10163 )
10164 .unwrap();
10165
10166 let mut buffer = Vec::new();
10167 {
10168 let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
10169 writer.write(&batch).unwrap();
10170 writer.finish().unwrap();
10171 }
10172
10173 let mut request = CreateTableRequest::new();
10174 let table_id = vec!["test_ops_table".to_string()];
10175 request.id = Some(table_id.clone());
10176
10177 namespace
10178 .create_table(request, Bytes::from(buffer))
10179 .await
10180 .unwrap();
10181
10182 (namespace, temp_dir, table_id)
10183 }
10184
10185 #[tokio::test]
10186 async fn test_count_table_rows_basic() {
10187 let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10188
10189 let request = CountTableRowsRequest {
10190 id: Some(table_id),
10191 version: None,
10192 predicate: None,
10193 ..Default::default()
10194 };
10195
10196 let count = namespace.count_table_rows(request).await.unwrap();
10197 assert_eq!(count, 3);
10198 }
10199
10200 #[tokio::test]
10201 async fn test_count_table_rows_with_predicate() {
10202 let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10203
10204 let request = CountTableRowsRequest {
10205 id: Some(table_id),
10206 version: None,
10207 predicate: Some("id > 1".to_string()),
10208 ..Default::default()
10209 };
10210
10211 let count = namespace.count_table_rows(request).await.unwrap();
10212 assert_eq!(count, 2);
10213 }
10214
10215 #[tokio::test]
10216 async fn test_query_table_invalid_distance_type() {
10217 let (namespace, _temp_dir, table_id) = create_ns_with_vector_table().await;
10218
10219 let vector = Box::new(lance_namespace::models::QueryTableRequestVector {
10220 single_vector: Some(vec![1.0, 0.0, 0.0, 0.0]),
10221 multi_vector: None,
10222 });
10223
10224 let request = QueryTableRequest {
10225 id: Some(table_id),
10226 k: 2,
10227 vector,
10228 vector_column: Some("vector".to_string()),
10229 distance_type: Some("invalid_metric".to_string()),
10230 filter: None,
10231 offset: None,
10232 version: None,
10233 ..Default::default()
10234 };
10235
10236 let result = namespace.query_table(request).await;
10237 assert!(result.is_err());
10238 let err_msg = result.unwrap_err().to_string();
10239 assert!(
10240 err_msg.contains("Unknown distance type"),
10241 "Expected error about unknown distance type, got: {}",
10242 err_msg
10243 );
10244 }
10245
10246 #[tokio::test]
10247 async fn test_insert_into_table_append() {
10248 use arrow::array::{Int32Array, StringArray};
10249 use arrow::ipc::writer::StreamWriter;
10250
10251 let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10252
10253 let schema = create_test_schema();
10255 let arrow_schema = convert_json_arrow_schema(&schema).unwrap();
10256 let arrow_schema = Arc::new(arrow_schema);
10257
10258 let id_array = Int32Array::from(vec![4, 5]);
10259 let name_array = StringArray::from(vec!["Dave", "Eve"]);
10260 let batch = arrow::record_batch::RecordBatch::try_new(
10261 arrow_schema.clone(),
10262 vec![Arc::new(id_array), Arc::new(name_array)],
10263 )
10264 .unwrap();
10265
10266 let mut buffer = Vec::new();
10267 {
10268 let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
10269 writer.write(&batch).unwrap();
10270 writer.finish().unwrap();
10271 }
10272
10273 let request = InsertIntoTableRequest {
10274 id: Some(table_id.clone()),
10275 mode: Some("append".to_string()),
10276 ..Default::default()
10277 };
10278
10279 let response = namespace
10280 .insert_into_table(request, Bytes::from(buffer))
10281 .await
10282 .unwrap();
10283 assert!(response.transaction_id.is_none());
10284
10285 let count_req = CountTableRowsRequest {
10287 id: Some(table_id),
10288 version: None,
10289 predicate: None,
10290 ..Default::default()
10291 };
10292 let count = namespace.count_table_rows(count_req).await.unwrap();
10293 assert_eq!(count, 5);
10294 }
10295
10296 #[tokio::test]
10297 async fn test_insert_into_table_overwrite() {
10298 use arrow::array::{Int32Array, StringArray};
10299 use arrow::ipc::writer::StreamWriter;
10300
10301 let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10302
10303 let schema = create_test_schema();
10304 let arrow_schema = convert_json_arrow_schema(&schema).unwrap();
10305 let arrow_schema = Arc::new(arrow_schema);
10306
10307 let id_array = Int32Array::from(vec![10, 20]);
10308 let name_array = StringArray::from(vec!["X", "Y"]);
10309 let batch = arrow::record_batch::RecordBatch::try_new(
10310 arrow_schema.clone(),
10311 vec![Arc::new(id_array), Arc::new(name_array)],
10312 )
10313 .unwrap();
10314
10315 let mut buffer = Vec::new();
10316 {
10317 let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
10318 writer.write(&batch).unwrap();
10319 writer.finish().unwrap();
10320 }
10321
10322 let request = InsertIntoTableRequest {
10323 id: Some(table_id.clone()),
10324 mode: Some("overwrite".to_string()),
10325 ..Default::default()
10326 };
10327
10328 namespace
10329 .insert_into_table(request, Bytes::from(buffer))
10330 .await
10331 .unwrap();
10332
10333 let count_req = CountTableRowsRequest {
10335 id: Some(table_id),
10336 version: None,
10337 predicate: None,
10338 ..Default::default()
10339 };
10340 let count = namespace.count_table_rows(count_req).await.unwrap();
10341 assert_eq!(count, 2);
10342 }
10343
10344 #[tokio::test]
10345 async fn test_insert_into_table_empty_data() {
10346 let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10347
10348 let request = InsertIntoTableRequest {
10349 id: Some(table_id),
10350 mode: None,
10351 ..Default::default()
10352 };
10353
10354 let result = namespace.insert_into_table(request, Bytes::new()).await;
10355 assert!(result.is_err());
10356 assert!(
10357 result
10358 .unwrap_err()
10359 .to_string()
10360 .contains("Arrow IPC stream) is required")
10361 );
10362 }
10363
10364 #[tokio::test]
10365 async fn test_insert_into_table_with_storage_options() {
10366 use arrow::array::{Int32Array, StringArray};
10367 use arrow::ipc::writer::StreamWriter;
10368
10369 let temp_dir = TempStdDir::default();
10370
10371 let namespace = DirectoryNamespaceBuilder::new(temp_dir.to_str().unwrap())
10373 .storage_option("allow_http", "true")
10374 .build()
10375 .await
10376 .unwrap();
10377
10378 let schema = create_test_schema();
10380 let ipc_data = create_test_ipc_data(&schema);
10381 let mut create_req = CreateTableRequest::new();
10382 let table_id = vec!["so_table".to_string()];
10383 create_req.id = Some(table_id.clone());
10384 namespace
10385 .create_table(create_req, Bytes::from(ipc_data))
10386 .await
10387 .unwrap();
10388
10389 let arrow_schema = convert_json_arrow_schema(&schema).unwrap();
10391 let arrow_schema = Arc::new(arrow_schema);
10392
10393 let id_array = Int32Array::from(vec![10, 20]);
10394 let name_array = StringArray::from(vec!["X", "Y"]);
10395 let batch = arrow::record_batch::RecordBatch::try_new(
10396 arrow_schema.clone(),
10397 vec![Arc::new(id_array), Arc::new(name_array)],
10398 )
10399 .unwrap();
10400
10401 let mut buffer = Vec::new();
10402 {
10403 let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
10404 writer.write(&batch).unwrap();
10405 writer.finish().unwrap();
10406 }
10407
10408 let request = InsertIntoTableRequest {
10409 id: Some(table_id.clone()),
10410 mode: Some("append".to_string()),
10411 ..Default::default()
10412 };
10413
10414 let response = namespace
10415 .insert_into_table(request, Bytes::from(buffer))
10416 .await
10417 .unwrap();
10418 assert!(response.transaction_id.is_none());
10419
10420 let count_req = CountTableRowsRequest {
10422 id: Some(table_id),
10423 version: None,
10424 predicate: None,
10425 ..Default::default()
10426 };
10427 let count = namespace.count_table_rows(count_req).await.unwrap();
10428 assert_eq!(count, 2);
10429 }
10430
10431 #[tokio::test]
10432 async fn test_query_table_basic() {
10433 let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10434
10435 let request = QueryTableRequest {
10436 id: Some(table_id),
10437 k: 10,
10438 filter: None,
10439 offset: None,
10440 version: None,
10441 ..Default::default()
10442 };
10443
10444 let bytes = namespace.query_table(request).await.unwrap();
10445
10446 let cursor = Cursor::new(bytes.to_vec());
10448 let reader = FileReader::try_new(cursor, None).unwrap();
10449 let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10450 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10451 assert_eq!(total_rows, 3);
10452 }
10453
10454 #[tokio::test]
10455 async fn test_query_table_with_filter() {
10456 let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10457
10458 let request = QueryTableRequest {
10459 id: Some(table_id),
10460 k: 10,
10461 filter: Some("id <= 2".to_string()),
10462 offset: None,
10463 version: None,
10464 ..Default::default()
10465 };
10466
10467 let bytes = namespace.query_table(request).await.unwrap();
10468
10469 let cursor = Cursor::new(bytes.to_vec());
10470 let reader = FileReader::try_new(cursor, None).unwrap();
10471 let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10472 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10473 assert_eq!(total_rows, 2);
10474 }
10475
10476 #[tokio::test]
10477 async fn test_query_table_with_limit_and_offset() {
10478 let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10479
10480 let request = QueryTableRequest {
10481 id: Some(table_id),
10482 k: 2,
10483 filter: None,
10484 offset: Some(1),
10485 version: None,
10486 ..Default::default()
10487 };
10488
10489 let bytes = namespace.query_table(request).await.unwrap();
10490
10491 let cursor = Cursor::new(bytes.to_vec());
10492 let reader = FileReader::try_new(cursor, None).unwrap();
10493 let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10494 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10495 assert_eq!(total_rows, 2);
10496 }
10497
10498 #[tokio::test]
10499 async fn test_query_table_no_limit() {
10500 let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10501
10502 let request = QueryTableRequest {
10504 id: Some(table_id),
10505 k: 0,
10506 filter: None,
10507 offset: None,
10508 version: None,
10509 ..Default::default()
10510 };
10511
10512 let bytes = namespace.query_table(request).await.unwrap();
10513
10514 let cursor = Cursor::new(bytes.to_vec());
10515 let reader = FileReader::try_new(cursor, None).unwrap();
10516 let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10517 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10518 assert_eq!(total_rows, 3);
10519 }
10520
10521 #[tokio::test]
10522 async fn test_query_table_with_columns() {
10523 let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10524
10525 let columns = Box::new(lance_namespace::models::QueryTableRequestColumns {
10526 column_names: Some(vec!["id".to_string()]),
10527 column_aliases: None,
10528 });
10529
10530 let request = QueryTableRequest {
10531 id: Some(table_id),
10532 k: 10,
10533 filter: None,
10534 offset: None,
10535 version: None,
10536 columns: Some(columns),
10537 ..Default::default()
10538 };
10539
10540 let bytes = namespace.query_table(request).await.unwrap();
10541
10542 let cursor = Cursor::new(bytes.to_vec());
10543 let reader = FileReader::try_new(cursor, None).unwrap();
10544 let schema = reader.schema();
10545 assert_eq!(schema.fields().len(), 1);
10546 assert_eq!(schema.field(0).name(), "id");
10547 let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10548 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10549 assert_eq!(total_rows, 3);
10550 }
10551
10552 #[tokio::test]
10553 async fn test_count_table_rows_with_version() {
10554 use arrow::array::{Int32Array, StringArray};
10555 use arrow::ipc::writer::StreamWriter;
10556
10557 let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10558
10559 let schema = create_test_schema();
10561 let arrow_schema = convert_json_arrow_schema(&schema).unwrap();
10562 let arrow_schema = Arc::new(arrow_schema);
10563
10564 let id_array = Int32Array::from(vec![4, 5]);
10565 let name_array = StringArray::from(vec!["Dave", "Eve"]);
10566 let batch = arrow::record_batch::RecordBatch::try_new(
10567 arrow_schema.clone(),
10568 vec![Arc::new(id_array), Arc::new(name_array)],
10569 )
10570 .unwrap();
10571
10572 let mut buffer = Vec::new();
10573 {
10574 let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
10575 writer.write(&batch).unwrap();
10576 writer.finish().unwrap();
10577 }
10578
10579 let request = InsertIntoTableRequest {
10580 id: Some(table_id.clone()),
10581 mode: None,
10582 ..Default::default()
10583 };
10584 namespace
10585 .insert_into_table(request, Bytes::from(buffer))
10586 .await
10587 .unwrap();
10588
10589 let count_req = CountTableRowsRequest {
10591 id: Some(table_id.clone()),
10592 version: Some(1),
10593 predicate: None,
10594 ..Default::default()
10595 };
10596 let count = namespace.count_table_rows(count_req).await.unwrap();
10597 assert_eq!(count, 3);
10598
10599 let count_req = CountTableRowsRequest {
10601 id: Some(table_id),
10602 version: None,
10603 predicate: None,
10604 ..Default::default()
10605 };
10606 let count = namespace.count_table_rows(count_req).await.unwrap();
10607 assert_eq!(count, 5);
10608 }
10609
10610 #[tokio::test]
10611 async fn test_query_table_with_version() {
10612 use arrow::array::{Int32Array, StringArray};
10613 use arrow::ipc::writer::StreamWriter;
10614
10615 let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10616
10617 let schema = create_test_schema();
10619 let arrow_schema = convert_json_arrow_schema(&schema).unwrap();
10620 let arrow_schema = Arc::new(arrow_schema);
10621
10622 let id_array = Int32Array::from(vec![4, 5]);
10623 let name_array = StringArray::from(vec!["Dave", "Eve"]);
10624 let batch = arrow::record_batch::RecordBatch::try_new(
10625 arrow_schema.clone(),
10626 vec![Arc::new(id_array), Arc::new(name_array)],
10627 )
10628 .unwrap();
10629
10630 let mut buffer = Vec::new();
10631 {
10632 let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
10633 writer.write(&batch).unwrap();
10634 writer.finish().unwrap();
10635 }
10636
10637 let request = InsertIntoTableRequest {
10638 id: Some(table_id.clone()),
10639 mode: None,
10640 ..Default::default()
10641 };
10642 namespace
10643 .insert_into_table(request, Bytes::from(buffer))
10644 .await
10645 .unwrap();
10646
10647 let request = QueryTableRequest {
10649 id: Some(table_id.clone()),
10650 k: 100,
10651 filter: None,
10652 offset: None,
10653 version: Some(1),
10654 ..Default::default()
10655 };
10656
10657 let bytes = namespace.query_table(request).await.unwrap();
10658 let cursor = Cursor::new(bytes.to_vec());
10659 let reader = FileReader::try_new(cursor, None).unwrap();
10660 let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10661 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10662 assert_eq!(total_rows, 3);
10663
10664 let request = QueryTableRequest {
10666 id: Some(table_id),
10667 k: 100,
10668 filter: None,
10669 offset: None,
10670 version: None,
10671 ..Default::default()
10672 };
10673
10674 let bytes = namespace.query_table(request).await.unwrap();
10675 let cursor = Cursor::new(bytes.to_vec());
10676 let reader = FileReader::try_new(cursor, None).unwrap();
10677 let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10678 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10679 assert_eq!(total_rows, 5);
10680 }
10681
10682 async fn create_ns_with_vector_table() -> (DirectoryNamespace, TempStdDir, Vec<String>) {
10685 use arrow::array::{FixedSizeListArray, Float32Array, Int32Array};
10686 use arrow::ipc::writer::StreamWriter;
10687
10688 let (namespace, temp_dir) = create_test_namespace().await;
10689
10690 let arrow_schema = Arc::new(arrow::datatypes::Schema::new(vec![
10692 arrow::datatypes::Field::new("id", arrow::datatypes::DataType::Int32, false),
10693 arrow::datatypes::Field::new(
10694 "vector",
10695 arrow::datatypes::DataType::FixedSizeList(
10696 Arc::new(arrow::datatypes::Field::new(
10697 "item",
10698 arrow::datatypes::DataType::Float32,
10699 true,
10700 )),
10701 4,
10702 ),
10703 true,
10704 ),
10705 ]));
10706
10707 let id_array = Int32Array::from(vec![1, 2, 3]);
10708 let values = Float32Array::from(vec![
10709 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, ]);
10713 let vector_array = FixedSizeListArray::try_new(
10714 Arc::new(arrow::datatypes::Field::new(
10715 "item",
10716 arrow::datatypes::DataType::Float32,
10717 true,
10718 )),
10719 4,
10720 Arc::new(values),
10721 None,
10722 )
10723 .unwrap();
10724
10725 let batch = arrow::record_batch::RecordBatch::try_new(
10726 arrow_schema.clone(),
10727 vec![Arc::new(id_array), Arc::new(vector_array)],
10728 )
10729 .unwrap();
10730
10731 let mut buffer = Vec::new();
10732 {
10733 let mut writer = StreamWriter::try_new(&mut buffer, &arrow_schema).unwrap();
10734 writer.write(&batch).unwrap();
10735 writer.finish().unwrap();
10736 }
10737
10738 let table_name = "vector_table";
10740 let table_uri = format!("{}/{}.lance", temp_dir.to_str().unwrap(), table_name);
10741 let reader = arrow::record_batch::RecordBatchIterator::new(
10742 vec![Ok(batch)],
10743 arrow_schema.clone(),
10744 );
10745 Dataset::write(reader, &table_uri, None).await.unwrap();
10746
10747 let table_id = vec![table_name.to_string()];
10748 (namespace, temp_dir, table_id)
10749 }
10750
10751 #[tokio::test]
10752 async fn test_query_table_vector_search() {
10753 let (namespace, _temp_dir, table_id) = create_ns_with_vector_table().await;
10754
10755 let vector = Box::new(lance_namespace::models::QueryTableRequestVector {
10756 single_vector: Some(vec![1.0, 0.0, 0.0, 0.0]),
10757 multi_vector: None,
10758 });
10759
10760 let request = QueryTableRequest {
10761 id: Some(table_id),
10762 k: 2,
10763 vector,
10764 filter: None,
10765 offset: None,
10766 version: None,
10767 ..Default::default()
10768 };
10769
10770 let bytes = namespace.query_table(request).await.unwrap();
10771
10772 let cursor = Cursor::new(bytes.to_vec());
10773 let reader = FileReader::try_new(cursor, None).unwrap();
10774 let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10775 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10776 assert_eq!(total_rows, 2);
10777 }
10778
10779 #[tokio::test]
10780 async fn test_query_table_vector_search_with_distance_type() {
10781 let (namespace, _temp_dir, table_id) = create_ns_with_vector_table().await;
10782
10783 let vector = Box::new(lance_namespace::models::QueryTableRequestVector {
10784 single_vector: Some(vec![1.0, 0.0, 0.0, 0.0]),
10785 multi_vector: None,
10786 });
10787
10788 let request = QueryTableRequest {
10789 id: Some(table_id),
10790 k: 3,
10791 vector,
10792 filter: None,
10793 offset: None,
10794 version: None,
10795 distance_type: Some("cosine".to_string()),
10796 ..Default::default()
10797 };
10798
10799 let bytes = namespace.query_table(request).await.unwrap();
10800
10801 let cursor = Cursor::new(bytes.to_vec());
10802 let reader = FileReader::try_new(cursor, None).unwrap();
10803 let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10804 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10805 assert_eq!(total_rows, 3);
10806 }
10807
10808 #[tokio::test]
10809 async fn test_query_table_vector_search_with_filter() {
10810 let (namespace, _temp_dir, table_id) = create_ns_with_vector_table().await;
10811
10812 let vector = Box::new(lance_namespace::models::QueryTableRequestVector {
10813 single_vector: Some(vec![1.0, 0.0, 0.0, 0.0]),
10814 multi_vector: None,
10815 });
10816
10817 let request = QueryTableRequest {
10818 id: Some(table_id),
10819 k: 10,
10820 vector,
10821 filter: Some("id <= 2".to_string()),
10822 offset: None,
10823 version: None,
10824 ..Default::default()
10825 };
10826
10827 let bytes = namespace.query_table(request).await.unwrap();
10828
10829 let cursor = Cursor::new(bytes.to_vec());
10830 let reader = FileReader::try_new(cursor, None).unwrap();
10831 let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10832 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10833 assert!(total_rows <= 2);
10834 }
10835
10836 #[tokio::test]
10837 async fn test_query_table_vector_search_with_nprobes_and_refine() {
10838 let (namespace, _temp_dir, table_id) = create_ns_with_vector_table().await;
10839
10840 let vector = Box::new(lance_namespace::models::QueryTableRequestVector {
10841 single_vector: Some(vec![0.0, 1.0, 0.0, 0.0]),
10842 multi_vector: None,
10843 });
10844
10845 let request = QueryTableRequest {
10846 id: Some(table_id),
10847 k: 2,
10848 vector,
10849 filter: None,
10850 offset: None,
10851 version: None,
10852 nprobes: Some(1),
10853 refine_factor: Some(1),
10854 prefilter: Some(true),
10855 ..Default::default()
10856 };
10857
10858 let bytes = namespace.query_table(request).await.unwrap();
10859
10860 let cursor = Cursor::new(bytes.to_vec());
10861 let reader = FileReader::try_new(cursor, None).unwrap();
10862 let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10863 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10864 assert_eq!(total_rows, 2);
10865 }
10866
10867 #[tokio::test]
10868 async fn test_namespace_id() {
10869 let (namespace, _temp_dir) = create_test_namespace().await;
10870 let id = namespace.namespace_id();
10871 assert!(id.contains("DirectoryNamespace"));
10872 assert!(id.contains("root"));
10873 }
10874
10875 #[tokio::test]
10876 async fn test_query_table_empty_table() {
10877 let (namespace, _temp_dir) = create_test_namespace().await;
10878
10879 let schema = create_test_schema();
10881 let ipc_data = create_test_ipc_data(&schema);
10882 let mut create_request = CreateTableRequest::new();
10883 create_request.id = Some(vec!["empty_table".to_string()]);
10884 namespace
10885 .create_table(create_request, bytes::Bytes::from(ipc_data))
10886 .await
10887 .unwrap();
10888
10889 let vector = Box::new(lance_namespace::models::QueryTableRequestVector {
10891 single_vector: None,
10892 multi_vector: None,
10893 });
10894 let request = QueryTableRequest {
10895 id: Some(vec!["empty_table".to_string()]),
10896 k: 10,
10897 vector,
10898 ..Default::default()
10899 };
10900 let bytes = namespace.query_table(request).await.unwrap();
10901
10902 let cursor = Cursor::new(bytes.to_vec());
10903 let reader = FileReader::try_new(cursor, None).unwrap();
10904 let batches: Vec<_> = reader.collect::<std::result::Result<Vec<_>, _>>().unwrap();
10905 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10906 assert_eq!(total_rows, 0, "empty table should yield no rows");
10907 }
10908
10909 #[tokio::test]
10910 async fn test_query_table_with_plain_filter_no_vector() {
10911 let (namespace, _temp_dir, table_id) = create_ns_with_table().await;
10912
10913 let vector = Box::new(lance_namespace::models::QueryTableRequestVector {
10915 single_vector: None,
10916 multi_vector: None,
10917 });
10918 let request = QueryTableRequest {
10919 id: Some(table_id),
10920 k: 0,
10921 vector,
10922 filter: Some("id > 1".to_string()),
10923 ..Default::default()
10924 };
10925 let bytes = namespace.query_table(request).await.unwrap();
10926
10927 let cursor = Cursor::new(bytes.to_vec());
10928 let reader = FileReader::try_new(cursor, None).unwrap();
10929 let batches: Vec<_> = reader.into_iter().map(|b| b.unwrap()).collect();
10930 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10931 assert!(total_rows > 0);
10932 assert!(total_rows < 3);
10933 }
10934 }
10935
10936 #[tokio::test]
10937 async fn test_list_all_tables() {
10938 use lance_namespace::models::ListTablesRequest;
10939
10940 let (namespace, _temp_dir) = create_test_namespace().await;
10941 create_scalar_table(&namespace, "alpha").await;
10942 create_scalar_table(&namespace, "beta").await;
10943
10944 let request = ListTablesRequest {
10945 id: Some(vec![]),
10946 page_token: None,
10947 limit: None,
10948 ..Default::default()
10949 };
10950 let response = namespace.list_all_tables(request).await.unwrap();
10951 let mut tables = response.tables;
10952 tables.sort();
10953 assert_eq!(tables, vec!["alpha", "beta"]);
10954 }
10955
10956 #[tokio::test]
10957 async fn test_restore_table() {
10958 use lance_namespace::models::RestoreTableRequest;
10959
10960 let (namespace, _temp_dir) = create_test_namespace().await;
10961 create_scalar_table(&namespace, "users").await;
10962
10963 create_scalar_index(&namespace, "users", "users_id_idx").await;
10965
10966 let dataset = open_dataset(&namespace, "users").await;
10967 let current_version = dataset.version().version;
10968 assert!(current_version >= 2, "Should have at least 2 versions");
10969
10970 let mut restore_req = RestoreTableRequest::new(1);
10972 restore_req.id = Some(vec!["users".to_string()]);
10973 let response = namespace.restore_table(restore_req).await.unwrap();
10974
10975 assert!(
10977 response.transaction_id.is_some(),
10978 "restore_table should return a transaction_id"
10979 );
10980
10981 let dataset_after = open_dataset(&namespace, "users").await;
10983 assert!(
10984 dataset_after.version().version > current_version,
10985 "Restore should create a new version"
10986 );
10987 }
10988
10989 #[tokio::test]
10990 async fn test_update_table_schema_metadata() {
10991 use lance_namespace::models::UpdateTableSchemaMetadataRequest;
10992
10993 let (namespace, _temp_dir) = create_test_namespace().await;
10994 create_scalar_table(&namespace, "products").await;
10995
10996 let mut metadata = HashMap::new();
10997 metadata.insert("owner".to_string(), "team_a".to_string());
10998 metadata.insert("version".to_string(), "1.0".to_string());
10999
11000 let mut req = UpdateTableSchemaMetadataRequest::new();
11001 req.id = Some(vec!["products".to_string()]);
11002 req.metadata = Some(metadata.clone());
11003
11004 let response = namespace.update_table_schema_metadata(req).await.unwrap();
11005
11006 assert!(response.metadata.is_some());
11007 let returned = response.metadata.unwrap();
11008 assert_eq!(returned.get("owner"), Some(&"team_a".to_string()));
11009 assert_eq!(returned.get("version"), Some(&"1.0".to_string()));
11010 assert!(
11011 response.transaction_id.is_some(),
11012 "update_table_schema_metadata should return a transaction_id"
11013 );
11014 }
11015
11016 #[tokio::test]
11017 async fn test_get_table_stats() {
11018 use lance_namespace::models::GetTableStatsRequest;
11019
11020 let (namespace, _temp_dir) = create_test_namespace().await;
11021 create_scalar_table(&namespace, "items").await;
11022 create_scalar_index(&namespace, "items", "items_id_idx").await;
11023
11024 let mut req = GetTableStatsRequest::new();
11025 req.id = Some(vec!["items".to_string()]);
11026
11027 let response = namespace.get_table_stats(req).await.unwrap();
11028 assert_eq!(response.num_rows, 3);
11029 assert_eq!(response.num_indices, 1);
11030 }
11031
11032 #[tokio::test]
11033 async fn test_explain_table_query_plan() {
11034 use lance_namespace::models::QueryTableRequestVector;
11035 use lance_namespace::models::{ExplainTableQueryPlanRequest, QueryTableRequest};
11036
11037 let (namespace, _temp_dir) = create_test_namespace().await;
11038 create_scalar_table(&namespace, "catalog").await;
11039
11040 let mut query = QueryTableRequest::new(1, QueryTableRequestVector::new());
11041 query.filter = Some("id > 1".to_string());
11042 query.columns = Some(Box::new(QueryTableRequestColumns {
11043 column_names: Some(vec!["id".to_string(), "name".to_string()]),
11044 column_aliases: None,
11045 }));
11046 query.with_row_id = Some(true);
11047
11048 let mut req = ExplainTableQueryPlanRequest::new(query);
11049 req.id = Some(vec!["catalog".to_string()]);
11050
11051 let plan_str = namespace.explain_table_query_plan(req).await.unwrap();
11052 assert_plan_contains_all(
11053 &plan_str,
11054 &[
11055 "ProjectionExec: expr=[id@0 as id, name@2 as name",
11056 "Take: columns=\"id, _rowid, (name)\"",
11057 "LanceRead: uri=",
11058 "projection=[id]",
11059 "row_id=true, row_addr=false",
11060 "full_filter=id > Int32(1)",
11061 "refine_filter=id > Int32(1)",
11062 ],
11063 "Filtered explain plan should preserve late materialization and filter pushdown",
11064 );
11065 }
11066
11067 #[tokio::test]
11068 async fn test_analyze_table_query_plan() {
11069 use lance_namespace::models::AnalyzeTableQueryPlanRequest;
11070 use lance_namespace::models::QueryTableRequestVector;
11071
11072 let (namespace, _temp_dir) = create_test_namespace().await;
11073 create_scalar_table(&namespace, "catalog").await;
11074
11075 let mut req = AnalyzeTableQueryPlanRequest::new(1, QueryTableRequestVector::new());
11076 req.id = Some(vec!["catalog".to_string()]);
11077 req.filter = Some("id > 0".to_string());
11078 req.columns = Some(Box::new(QueryTableRequestColumns {
11079 column_names: Some(vec!["id".to_string(), "name".to_string()]),
11080 column_aliases: None,
11081 }));
11082 req.with_row_id = Some(true);
11083
11084 let analysis_str = namespace.analyze_table_query_plan(req).await.unwrap();
11085 assert_plan_contains_all(
11086 &analysis_str,
11087 &[
11088 "AnalyzeExec verbose=true",
11089 "ProjectionExec: elapsed=",
11090 "expr=[id@0 as id, name@2 as name",
11091 "Take: elapsed=",
11092 "columns=\"id, _rowid, (name)\"",
11093 "CoalesceBatchesExec: elapsed=",
11094 "LanceRead: elapsed=",
11095 "projection=[id]",
11096 "row_id=true, row_addr=false",
11097 "full_filter=id > Int32(0)",
11098 "refine_filter=id > Int32(0)",
11099 "metrics=[output_rows=",
11100 ],
11101 "Filtered analyze plan should preserve late materialization and filter pushdown",
11102 );
11103 }
11104
11105 #[tokio::test]
11106 async fn test_dir_listing_no_extra_calls_without_migration() {
11107 let temp_dir = TempStdDir::default();
11108 let temp_path = temp_dir.to_str().unwrap();
11109 let root_uri = file_object_store_uri(temp_path);
11110 let listing_count = Arc::new(AtomicUsize::new(0));
11111 let session = build_listing_counting_session(listing_count.clone());
11112
11113 let dir_only_ns = DirectoryNamespaceBuilder::new(root_uri.clone())
11115 .session(session.clone())
11116 .manifest_enabled(false)
11117 .dir_listing_enabled(true)
11118 .build()
11119 .await
11120 .unwrap();
11121
11122 let schema = create_test_schema();
11123 let ipc_data = create_test_ipc_data(&schema);
11124 let mut create_req = CreateTableRequest::new();
11125 create_req.id = Some(vec!["test_table".to_string()]);
11126 dir_only_ns
11127 .create_table(create_req, Bytes::from(ipc_data))
11128 .await
11129 .unwrap();
11130
11131 let hybrid_ns = DirectoryNamespaceBuilder::new(root_uri)
11133 .session(session)
11134 .manifest_enabled(true)
11135 .dir_listing_enabled(true)
11136 .dir_listing_to_manifest_migration_enabled(false)
11137 .build()
11138 .await
11139 .unwrap();
11140
11141 listing_count.store(0, Ordering::SeqCst);
11143
11144 let mut exists_req = TableExistsRequest::new();
11146 exists_req.id = Some(vec!["test_table".to_string()]);
11147 hybrid_ns.table_exists(exists_req).await.unwrap();
11148
11149 let count = listing_count.load(Ordering::SeqCst);
11150 assert_eq!(
11151 count, 1,
11152 "Expected exactly 1 listing call for table_exists \
11153 without migration mode, but got {}",
11154 count
11155 );
11156
11157 listing_count.store(0, Ordering::SeqCst);
11159
11160 let mut describe_req = DescribeTableRequest::new();
11161 describe_req.id = Some(vec!["test_table".to_string()]);
11162 hybrid_ns.describe_table(describe_req).await.unwrap();
11163
11164 let count = listing_count.load(Ordering::SeqCst);
11165 assert_eq!(
11166 count, 1,
11167 "Expected exactly 1 listing call for describe_table \
11168 without migration mode, but got {}",
11169 count
11170 );
11171 }
11172
11173 #[tokio::test]
11174 async fn test_describe_declared_table_checks_versions_only_when_requested() {
11175 let temp_dir = TempStdDir::default();
11176 let temp_path = temp_dir.to_str().unwrap();
11177 let root_uri = file_object_store_uri(temp_path);
11178 let listing_count = Arc::new(AtomicUsize::new(0));
11179 let session = build_listing_counting_session(listing_count.clone());
11180
11181 let namespace = DirectoryNamespaceBuilder::new(root_uri)
11182 .session(session)
11183 .manifest_enabled(false)
11184 .dir_listing_enabled(true)
11185 .build()
11186 .await
11187 .unwrap();
11188
11189 let mut declare_req = DeclareTableRequest::new();
11190 declare_req.id = Some(vec!["test_table".to_string()]);
11191 namespace.declare_table(declare_req).await.unwrap();
11192
11193 listing_count.store(0, Ordering::SeqCst);
11194
11195 let mut describe_req = DescribeTableRequest::new();
11196 describe_req.id = Some(vec!["test_table".to_string()]);
11197 let describe_response = namespace.describe_table(describe_req).await.unwrap();
11198
11199 assert_eq!(describe_response.is_only_declared, None);
11200 assert_eq!(
11201 listing_count.load(Ordering::SeqCst),
11202 1,
11203 "Default describe_table should only list the table directory"
11204 );
11205
11206 listing_count.store(0, Ordering::SeqCst);
11207
11208 let mut describe_req = DescribeTableRequest::new();
11209 describe_req.id = Some(vec!["test_table".to_string()]);
11210 describe_req.check_declared = Some(true);
11211 let describe_response = namespace.describe_table(describe_req).await.unwrap();
11212
11213 assert_eq!(describe_response.is_only_declared, Some(true));
11214 assert_eq!(
11215 listing_count.load(Ordering::SeqCst),
11216 2,
11217 "check_declared describe_table should list the table directory and _versions"
11218 );
11219 }
11220
11221 #[tokio::test]
11222 async fn test_dir_listing_extra_calls_with_migration() {
11223 let temp_dir = TempStdDir::default();
11224 let temp_path = temp_dir.to_str().unwrap();
11225 let root_uri = file_object_store_uri(temp_path);
11226 let listing_count = Arc::new(AtomicUsize::new(0));
11227 let session = build_listing_counting_session(listing_count.clone());
11228
11229 let dir_only_ns = DirectoryNamespaceBuilder::new(root_uri.clone())
11231 .session(session.clone())
11232 .manifest_enabled(false)
11233 .dir_listing_enabled(true)
11234 .build()
11235 .await
11236 .unwrap();
11237
11238 let schema = create_test_schema();
11239 let ipc_data = create_test_ipc_data(&schema);
11240 let mut create_req = CreateTableRequest::new();
11241 create_req.id = Some(vec!["test_table".to_string()]);
11242 dir_only_ns
11243 .create_table(create_req, Bytes::from(ipc_data))
11244 .await
11245 .unwrap();
11246
11247 let hybrid_ns = DirectoryNamespaceBuilder::new(root_uri)
11248 .session(session)
11249 .manifest_enabled(true)
11250 .dir_listing_enabled(true)
11251 .dir_listing_to_manifest_migration_enabled(true)
11252 .build()
11253 .await
11254 .unwrap();
11255
11256 listing_count.store(0, Ordering::SeqCst);
11260
11261 let mut exists_req = TableExistsRequest::new();
11262 exists_req.id = Some(vec!["test_table".to_string()]);
11263 hybrid_ns.table_exists(exists_req).await.unwrap();
11264
11265 let count = listing_count.load(Ordering::SeqCst);
11266 assert_eq!(
11267 count, 1,
11268 "Expected exactly 1 listing call for table_exists with migration mode \
11269 (table directory fallback; manifest reload uses the version hint), but got {}",
11270 count
11271 );
11272
11273 listing_count.store(0, Ordering::SeqCst);
11275
11276 let mut describe_req = DescribeTableRequest::new();
11277 describe_req.id = Some(vec!["test_table".to_string()]);
11278 hybrid_ns.describe_table(describe_req).await.unwrap();
11279
11280 let count = listing_count.load(Ordering::SeqCst);
11281 assert_eq!(
11282 count, 1,
11283 "Expected exactly 1 listing call for describe_table with migration mode \
11284 (table directory fallback; manifest reload uses the version hint), but got {}",
11285 count
11286 );
11287 }
11288
11289 #[tokio::test]
11290 async fn test_manifest_reload_observes_new_version_from_other_namespace() {
11291 let temp_dir = TempStdDir::default();
11292 let temp_path = temp_dir.to_str().unwrap();
11293
11294 let namespace_a = DirectoryNamespaceBuilder::new(temp_path)
11295 .manifest_enabled(true)
11296 .dir_listing_enabled(false)
11297 .build()
11298 .await
11299 .unwrap();
11300 create_scalar_table(&namespace_a, "alpha").await;
11301
11302 let namespace_b = DirectoryNamespaceBuilder::new(temp_path)
11303 .manifest_enabled(true)
11304 .dir_listing_enabled(false)
11305 .build()
11306 .await
11307 .unwrap();
11308 create_scalar_table(&namespace_b, "beta").await;
11309
11310 let response = namespace_a
11311 .list_tables(ListTablesRequest {
11312 id: Some(vec![]),
11313 ..Default::default()
11314 })
11315 .await
11316 .unwrap();
11317
11318 let mut tables = response.tables;
11319 tables.sort();
11320 assert_eq!(tables, vec!["alpha", "beta"]);
11321 }
11322
11323 #[tokio::test]
11324 async fn test_migration_not_found_errors_include_table_id() {
11325 let temp_dir = TempStdDir::default();
11326 let temp_path = temp_dir.to_str().unwrap();
11327
11328 let namespace = DirectoryNamespaceBuilder::new(temp_path)
11329 .manifest_enabled(true)
11330 .dir_listing_enabled(true)
11331 .dir_listing_to_manifest_migration_enabled(true)
11332 .build()
11333 .await
11334 .unwrap();
11335
11336 let mut exists_req = TableExistsRequest::new();
11337 exists_req.id = Some(vec!["missing_table".to_string()]);
11338 let err = namespace.table_exists(exists_req).await.unwrap_err();
11339 assert!(matches!(err, Error::Namespace { .. }));
11340 let err_msg = err.to_string();
11341 assert!(err_msg.contains("Table not found"));
11342 assert!(err_msg.contains("table id 'missing_table'"));
11343
11344 let mut describe_req = DescribeTableRequest::new();
11345 describe_req.id = Some(vec!["missing_table".to_string()]);
11346 let err = namespace.describe_table(describe_req).await.unwrap_err();
11347 assert!(matches!(err, Error::Namespace { .. }));
11348 let err_msg = err.to_string();
11349 assert!(err_msg.contains("Table not found"));
11350 assert!(err_msg.contains("table id 'missing_table'"));
11351 }
11352
11353 #[tokio::test]
11354 async fn test_manifest_not_found_errors_include_full_table_id() {
11355 use lance_namespace::models::CreateNamespaceRequest;
11356
11357 let temp_dir = TempStdDir::default();
11358 let temp_path = temp_dir.to_str().unwrap();
11359
11360 let namespace = DirectoryNamespaceBuilder::new(temp_path)
11361 .manifest_enabled(true)
11362 .dir_listing_enabled(true)
11363 .build()
11364 .await
11365 .unwrap();
11366
11367 let mut create_ns_req = CreateNamespaceRequest::new();
11368 create_ns_req.id = Some(vec!["workspace".to_string()]);
11369 namespace.create_namespace(create_ns_req).await.unwrap();
11370
11371 let missing_table_id = vec!["workspace".to_string(), "missing_table".to_string()];
11372
11373 let mut exists_req = TableExistsRequest::new();
11374 exists_req.id = Some(missing_table_id.clone());
11375 let err = namespace.table_exists(exists_req).await.unwrap_err();
11376 assert!(matches!(err, Error::Namespace { .. }));
11377 let err_msg = err.to_string();
11378 assert!(err_msg.contains("Table not found"));
11379 assert!(err_msg.contains("table id 'workspace$missing_table'"));
11380
11381 let mut describe_req = DescribeTableRequest::new();
11382 describe_req.id = Some(missing_table_id);
11383 let err = namespace.describe_table(describe_req).await.unwrap_err();
11384 assert!(matches!(err, Error::Namespace { .. }));
11385 let err_msg = err.to_string();
11386 assert!(err_msg.contains("Table not found"));
11387 assert!(err_msg.contains("table id 'workspace$missing_table'"));
11388 }
11389
11390 async fn create_tagged_test_table(
11393 versions: u32,
11394 ) -> (Arc<DirectoryNamespace>, TempStdDir, Vec<String>) {
11395 use arrow::array::{Int32Array, RecordBatchIterator};
11396 use arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
11397 use arrow::record_batch::RecordBatch;
11398 use lance::dataset::{Dataset, WriteMode, WriteParams};
11399
11400 assert!(versions >= 1, "versions must be at least 1");
11401
11402 let temp_dir = TempStdDir::default();
11403 let temp_path = temp_dir.to_str().unwrap();
11404
11405 let namespace = Arc::new(
11406 DirectoryNamespaceBuilder::new(temp_path)
11407 .build()
11408 .await
11409 .unwrap(),
11410 );
11411 let table_id = vec!["tag_table".to_string()];
11412 let arrow_schema = Arc::new(ArrowSchema::new(vec![Field::new(
11413 "id",
11414 DataType::Int32,
11415 false,
11416 )]));
11417 let initial_batch = RecordBatch::try_new(
11418 arrow_schema.clone(),
11419 vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
11420 )
11421 .unwrap();
11422 let batches = RecordBatchIterator::new(vec![Ok(initial_batch)], arrow_schema.clone());
11423 let write_params = WriteParams {
11424 mode: WriteMode::Create,
11425 ..Default::default()
11426 };
11427
11428 let mut dataset = Dataset::write_into_namespace(
11429 batches,
11430 namespace.clone() as Arc<dyn LanceNamespace>,
11431 table_id.clone(),
11432 Some(write_params),
11433 )
11434 .await
11435 .unwrap();
11436
11437 for i in 1..versions {
11438 let value_start = (i as i32) * 10;
11439 let batch = RecordBatch::try_new(
11440 arrow_schema.clone(),
11441 vec![Arc::new(Int32Array::from(vec![
11442 value_start,
11443 value_start + 1,
11444 ]))],
11445 )
11446 .unwrap();
11447 let batches = RecordBatchIterator::new(vec![Ok(batch)], arrow_schema.clone());
11448 dataset.append(batches, None).await.unwrap();
11449 }
11450
11451 (namespace, temp_dir, table_id)
11452 }
11453
11454 fn namespace_code(err: &Error) -> Option<ErrorCode> {
11456 match err {
11457 Error::Namespace { source, .. } => {
11458 source.downcast_ref::<NamespaceError>().map(|e| e.code())
11459 }
11460 _ => None,
11461 }
11462 }
11463
11464 #[tokio::test]
11465 async fn test_create_and_list_branches() {
11466 let (namespace, _temp_dir, table_id) = create_tagged_test_table(3).await;
11467
11468 namespace
11469 .create_table_branch(CreateTableBranchRequest {
11470 id: Some(table_id.clone()),
11471 name: "dev".to_string(),
11472 ..Default::default()
11473 })
11474 .await
11475 .unwrap();
11476 namespace
11477 .create_table_branch(CreateTableBranchRequest {
11478 id: Some(table_id.clone()),
11479 name: "staging".to_string(),
11480 ..Default::default()
11481 })
11482 .await
11483 .unwrap();
11484
11485 let resp = namespace
11486 .list_table_branches(ListTableBranchesRequest {
11487 id: Some(table_id.clone()),
11488 ..Default::default()
11489 })
11490 .await
11491 .unwrap();
11492 assert_eq!(
11493 resp.branches.len(),
11494 2,
11495 "expected 2 branches, got: {:?}",
11496 resp.branches
11497 );
11498 assert!(resp.branches.contains_key("dev"));
11499 assert!(resp.branches.contains_key("staging"));
11500 assert!(resp.page_token.is_none());
11501
11502 namespace
11504 .delete_table_branch(DeleteTableBranchRequest {
11505 id: Some(table_id.clone()),
11506 name: "dev".to_string(),
11507 ..Default::default()
11508 })
11509 .await
11510 .unwrap();
11511
11512 let resp = namespace
11513 .list_table_branches(ListTableBranchesRequest {
11514 id: Some(table_id),
11515 ..Default::default()
11516 })
11517 .await
11518 .unwrap();
11519 assert_eq!(resp.branches.len(), 1, "expected 1 branch after delete");
11520 assert!(!resp.branches.contains_key("dev"));
11521 assert!(resp.branches.contains_key("staging"));
11522 }
11523
11524 #[tokio::test]
11525 async fn test_create_branch_from_version() {
11526 let (namespace, _temp_dir, table_id) = create_tagged_test_table(3).await;
11527
11528 namespace
11530 .create_table_branch(CreateTableBranchRequest {
11531 id: Some(table_id.clone()),
11532 name: "from-v1".to_string(),
11533 from_version: Some(1),
11534 ..Default::default()
11535 })
11536 .await
11537 .unwrap();
11538
11539 let resp = namespace
11540 .list_table_branches(ListTableBranchesRequest {
11541 id: Some(table_id),
11542 ..Default::default()
11543 })
11544 .await
11545 .unwrap();
11546 let branch = resp
11547 .branches
11548 .get("from-v1")
11549 .expect("forked branch should be listed");
11550 assert_eq!(
11551 branch.parent_version, 1,
11552 "branch should fork from version 1"
11553 );
11554 assert!(
11555 branch.parent_branch.is_none(),
11556 "a branch forked from main has no parent branch"
11557 );
11558 }
11559
11560 #[tokio::test]
11565 async fn test_create_branch_from_other_branch() {
11566 use lance::dataset::builder::DatasetBuilder;
11567
11568 let (namespace, _temp_dir) = create_test_namespace().await;
11569 create_scalar_table(&namespace, "users").await; create_branch_with_commits(&namespace, "users", "dev", 1).await;
11572 let main_ds = open_dataset(&namespace, "users").await;
11574 append_scalar_version(main_ds.uri(), 500).await; namespace
11577 .create_table_branch(CreateTableBranchRequest {
11578 id: Some(vec!["users".to_string()]),
11579 name: "child".to_string(),
11580 from_branch: Some("dev".to_string()),
11581 from_version: Some(2),
11582 ..Default::default()
11583 })
11584 .await
11585 .unwrap();
11586
11587 let child_ds = DatasetBuilder::from_uri(main_ds.uri())
11588 .with_branch("child", None)
11589 .load()
11590 .await
11591 .unwrap();
11592 let ids = scan_id_column(&child_ds).await;
11593 assert!(
11594 ids.contains(&100) && ids.contains(&101),
11595 "child must contain dev's appended rows, got: {:?}",
11596 ids
11597 );
11598 assert!(
11599 !ids.contains(&500),
11600 "child must not contain main's diverged rows, got: {:?}",
11601 ids
11602 );
11603
11604 let listed = namespace
11606 .list_table_branches(ListTableBranchesRequest {
11607 id: Some(vec!["users".to_string()]),
11608 ..Default::default()
11609 })
11610 .await
11611 .unwrap();
11612 assert_eq!(
11613 listed
11614 .branches
11615 .get("child")
11616 .unwrap()
11617 .parent_branch
11618 .as_deref(),
11619 Some("dev")
11620 );
11621 }
11622
11623 #[tokio::test]
11624 async fn test_create_existing_branch_conflict() {
11625 let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11626
11627 namespace
11628 .create_table_branch(CreateTableBranchRequest {
11629 id: Some(table_id.clone()),
11630 name: "dev".to_string(),
11631 ..Default::default()
11632 })
11633 .await
11634 .unwrap();
11635
11636 let err = namespace
11637 .create_table_branch(CreateTableBranchRequest {
11638 id: Some(table_id),
11639 name: "dev".to_string(),
11640 ..Default::default()
11641 })
11642 .await
11643 .unwrap_err();
11644 assert_eq!(
11645 namespace_code(&err),
11646 Some(ErrorCode::TableBranchAlreadyExists),
11647 "expected TableBranchAlreadyExists, got: {}",
11648 err
11649 );
11650 assert!(
11651 err.to_string().to_lowercase().contains("already exists"),
11652 "expected already-exists message, got: {}",
11653 err
11654 );
11655 }
11656
11657 #[tokio::test]
11658 async fn test_delete_unknown_branch() {
11659 let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11660
11661 let err = namespace
11662 .delete_table_branch(DeleteTableBranchRequest {
11663 id: Some(table_id),
11664 name: "does-not-exist".to_string(),
11665 ..Default::default()
11666 })
11667 .await
11668 .unwrap_err();
11669 assert_eq!(
11670 namespace_code(&err),
11671 Some(ErrorCode::TableBranchNotFound),
11672 "expected TableBranchNotFound, got: {}",
11673 err
11674 );
11675 assert!(
11676 err.to_string().to_lowercase().contains("not found"),
11677 "expected not-found message, got: {}",
11678 err
11679 );
11680 }
11681
11682 #[tokio::test]
11683 async fn test_delete_referenced_branch_conflict() {
11684 let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11685
11686 namespace
11688 .create_table_branch(CreateTableBranchRequest {
11689 id: Some(table_id.clone()),
11690 name: "parent".to_string(),
11691 ..Default::default()
11692 })
11693 .await
11694 .unwrap();
11695 namespace
11696 .create_table_branch(CreateTableBranchRequest {
11697 id: Some(table_id.clone()),
11698 name: "child".to_string(),
11699 from_branch: Some("parent".to_string()),
11700 ..Default::default()
11701 })
11702 .await
11703 .unwrap();
11704
11705 let listed = namespace
11707 .list_table_branches(ListTableBranchesRequest {
11708 id: Some(table_id.clone()),
11709 ..Default::default()
11710 })
11711 .await
11712 .unwrap();
11713 let child = listed
11714 .branches
11715 .get("child")
11716 .expect("child branch should be listed");
11717 assert_eq!(
11718 child.parent_branch.as_deref(),
11719 Some("parent"),
11720 "child should record parent branch as its fork point"
11721 );
11722 assert!(
11723 child.parent_version >= 1,
11724 "child should record the parent version it forked from, got {}",
11725 child.parent_version
11726 );
11727
11728 let err = namespace
11731 .delete_table_branch(DeleteTableBranchRequest {
11732 id: Some(table_id),
11733 name: "parent".to_string(),
11734 ..Default::default()
11735 })
11736 .await
11737 .unwrap_err();
11738 assert_eq!(
11739 namespace_code(&err),
11740 Some(ErrorCode::InvalidInput),
11741 "expected InvalidInput for deleting a referenced branch, got: {}",
11742 err
11743 );
11744 assert!(
11745 err.to_string().to_lowercase().contains("referenced"),
11746 "error should explain the branch is still referenced, got: {}",
11747 err
11748 );
11749 }
11750
11751 #[tokio::test]
11752 async fn test_branch_name_required() {
11753 let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11754
11755 let create_err = namespace
11756 .create_table_branch(CreateTableBranchRequest {
11757 id: Some(table_id.clone()),
11758 name: String::new(),
11759 ..Default::default()
11760 })
11761 .await
11762 .unwrap_err();
11763 assert_eq!(
11764 namespace_code(&create_err),
11765 Some(ErrorCode::InvalidInput),
11766 "empty name on create should be InvalidInput, got: {}",
11767 create_err
11768 );
11769 assert!(
11770 create_err
11771 .to_string()
11772 .to_lowercase()
11773 .contains("must not be empty")
11774 );
11775
11776 let delete_err = namespace
11777 .delete_table_branch(DeleteTableBranchRequest {
11778 id: Some(table_id),
11779 name: String::new(),
11780 ..Default::default()
11781 })
11782 .await
11783 .unwrap_err();
11784 assert_eq!(
11785 namespace_code(&delete_err),
11786 Some(ErrorCode::InvalidInput),
11787 "empty name on delete should be InvalidInput, got: {}",
11788 delete_err
11789 );
11790 assert!(
11791 delete_err
11792 .to_string()
11793 .to_lowercase()
11794 .contains("must not be empty")
11795 );
11796 }
11797
11798 #[tokio::test]
11799 async fn test_create_branch_rejects_negative_from_version() {
11800 let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11801
11802 let err = namespace
11803 .create_table_branch(CreateTableBranchRequest {
11804 id: Some(table_id),
11805 name: "dev".to_string(),
11806 from_version: Some(-1),
11807 ..Default::default()
11808 })
11809 .await
11810 .unwrap_err();
11811 assert_eq!(
11812 namespace_code(&err),
11813 Some(ErrorCode::InvalidInput),
11814 "negative from_version should be InvalidInput, got: {}",
11815 err
11816 );
11817 assert!(err.to_string().to_lowercase().contains("from_version"));
11818 }
11819
11820 #[tokio::test]
11821 async fn test_create_branch_nonexistent_from_version() {
11822 let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11823
11824 let err = namespace
11827 .create_table_branch(CreateTableBranchRequest {
11828 id: Some(table_id),
11829 name: "dev".to_string(),
11830 from_version: Some(999),
11831 ..Default::default()
11832 })
11833 .await
11834 .unwrap_err();
11835 assert_eq!(
11836 namespace_code(&err),
11837 Some(ErrorCode::InvalidInput),
11838 "non-existent from_version should map to InvalidInput, got: {}",
11839 err
11840 );
11841 assert!(
11842 err.to_string().to_lowercase().contains("does not exist"),
11843 "error should name the missing source, got: {}",
11844 err
11845 );
11846 }
11847
11848 #[tokio::test]
11849 async fn test_create_and_list_tags() {
11850 let (namespace, _temp_dir, table_id) = create_tagged_test_table(3).await;
11851
11852 let mut req = CreateTableTagRequest::new("v1".to_string(), 1);
11853 req.id = Some(table_id.clone());
11854 namespace.create_table_tag(req).await.unwrap();
11855
11856 let mut req = CreateTableTagRequest::new("v2".to_string(), 2);
11857 req.id = Some(table_id.clone());
11858 namespace.create_table_tag(req).await.unwrap();
11859
11860 let mut list_req = ListTableTagsRequest::new();
11861 list_req.id = Some(table_id);
11862 let resp = namespace.list_table_tags(list_req).await.unwrap();
11863
11864 assert_eq!(resp.tags.len(), 2, "expected 2 tags, got: {:?}", resp.tags);
11865 assert_eq!(resp.tags.get("v1").unwrap().version, 1);
11866 assert_eq!(resp.tags.get("v2").unwrap().version, 2);
11867 assert!(resp.page_token.is_none());
11868 }
11869
11870 #[tokio::test]
11871 async fn test_create_existing_tag_conflict() {
11872 let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11873
11874 let mut req = CreateTableTagRequest::new("v1".to_string(), 1);
11875 req.id = Some(table_id.clone());
11876 namespace.create_table_tag(req).await.unwrap();
11877
11878 let mut req = CreateTableTagRequest::new("v1".to_string(), 2);
11879 req.id = Some(table_id);
11880 let err = namespace.create_table_tag(req).await.unwrap_err();
11881 assert!(
11882 err.to_string().to_lowercase().contains("already exists"),
11883 "expected already-exists error, got: {}",
11884 err
11885 );
11886 }
11887
11888 #[tokio::test]
11889 async fn test_get_tag_version() {
11890 let (namespace, _temp_dir, table_id) = create_tagged_test_table(3).await;
11891
11892 let mut req = CreateTableTagRequest::new("release".to_string(), 2);
11893 req.id = Some(table_id.clone());
11894 namespace.create_table_tag(req).await.unwrap();
11895
11896 let mut get_req = GetTableTagVersionRequest::new("release".to_string());
11897 get_req.id = Some(table_id);
11898 let resp = namespace.get_table_tag_version(get_req).await.unwrap();
11899 assert_eq!(resp.version, 2);
11900 assert_eq!(resp.branch, None);
11901 }
11902
11903 #[tokio::test]
11904 async fn test_get_unknown_tag() {
11905 let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11906
11907 let mut get_req = GetTableTagVersionRequest::new("does-not-exist".to_string());
11908 get_req.id = Some(table_id);
11909 let err = namespace.get_table_tag_version(get_req).await.unwrap_err();
11910 assert!(
11911 err.to_string().to_lowercase().contains("not found"),
11912 "expected not-found error, got: {}",
11913 err
11914 );
11915 }
11916
11917 #[tokio::test]
11918 async fn test_update_tag_to_new_version() {
11919 let (namespace, _temp_dir, table_id) = create_tagged_test_table(3).await;
11920
11921 let mut req = CreateTableTagRequest::new("rolling".to_string(), 1);
11922 req.id = Some(table_id.clone());
11923 namespace.create_table_tag(req).await.unwrap();
11924
11925 let mut update_req = UpdateTableTagRequest::new("rolling".to_string(), 3);
11926 update_req.id = Some(table_id.clone());
11927 namespace.update_table_tag(update_req).await.unwrap();
11928
11929 let mut get_req = GetTableTagVersionRequest::new("rolling".to_string());
11930 get_req.id = Some(table_id);
11931 let resp = namespace.get_table_tag_version(get_req).await.unwrap();
11932 assert_eq!(resp.version, 3);
11933 }
11934
11935 #[tokio::test]
11936 async fn test_update_unknown_tag() {
11937 let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11938
11939 let mut update_req = UpdateTableTagRequest::new("ghost".to_string(), 1);
11940 update_req.id = Some(table_id);
11941 let err = namespace.update_table_tag(update_req).await.unwrap_err();
11942 assert!(
11943 err.to_string().to_lowercase().contains("not found"),
11944 "expected not-found error, got: {}",
11945 err
11946 );
11947 }
11948
11949 #[tokio::test]
11950 async fn test_delete_tag() {
11951 let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11952
11953 let mut req = CreateTableTagRequest::new("doomed".to_string(), 1);
11954 req.id = Some(table_id.clone());
11955 namespace.create_table_tag(req).await.unwrap();
11956
11957 let mut delete_req = DeleteTableTagRequest::new("doomed".to_string());
11958 delete_req.id = Some(table_id.clone());
11959 namespace.delete_table_tag(delete_req).await.unwrap();
11960
11961 let mut list_req = ListTableTagsRequest::new();
11962 list_req.id = Some(table_id.clone());
11963 let resp = namespace.list_table_tags(list_req).await.unwrap();
11964 assert!(resp.tags.is_empty(), "tag should be removed after delete");
11965
11966 let mut get_req = GetTableTagVersionRequest::new("doomed".to_string());
11968 get_req.id = Some(table_id);
11969 let err = namespace.get_table_tag_version(get_req).await.unwrap_err();
11970 assert!(err.to_string().to_lowercase().contains("not found"));
11971 }
11972
11973 #[tokio::test]
11974 async fn test_delete_unknown_tag() {
11975 let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11976
11977 let mut delete_req = DeleteTableTagRequest::new("nope".to_string());
11978 delete_req.id = Some(table_id);
11979 let err = namespace.delete_table_tag(delete_req).await.unwrap_err();
11980 assert!(
11981 err.to_string().to_lowercase().contains("not found"),
11982 "expected not-found error, got: {}",
11983 err
11984 );
11985 }
11986
11987 #[tokio::test]
11988 async fn test_create_tag_invalid_version() {
11989 let (namespace, _temp_dir, table_id) = create_tagged_test_table(2).await;
11990
11991 let mut req = CreateTableTagRequest::new("v0".to_string(), 0);
11993 req.id = Some(table_id.clone());
11994 let err = namespace.create_table_tag(req).await.unwrap_err();
11995 assert!(
11996 err.to_string().to_lowercase().contains("positive"),
11997 "expected positive-version error, got: {}",
11998 err
11999 );
12000
12001 let mut req = CreateTableTagRequest::new(String::new(), 1);
12003 req.id = Some(table_id);
12004 let err = namespace.create_table_tag(req).await.unwrap_err();
12005 assert!(
12006 err.to_string().to_lowercase().contains("must not be empty"),
12007 "expected empty-tag-name error, got: {}",
12008 err
12009 );
12010 }
12011
12012 #[tokio::test]
12013 async fn test_create_tag_table_not_found() {
12014 let (namespace, _temp_dir) = create_test_namespace().await;
12015
12016 let mut req = CreateTableTagRequest::new("v1".to_string(), 1);
12017 req.id = Some(vec!["does_not_exist".to_string()]);
12018 let err = namespace.create_table_tag(req).await.unwrap_err();
12019 let msg = err.to_string();
12020 assert!(
12021 msg.contains("Table") && msg.to_lowercase().contains("not found"),
12022 "expected TableNotFound error, got: {}",
12023 err
12024 );
12025 }
12026}