diff --git a/python/python/lancedb/namespace.py b/python/python/lancedb/namespace.py index a6dd8914f..cb2d9b5a7 100644 --- a/python/python/lancedb/namespace.py +++ b/python/python/lancedb/namespace.py @@ -58,6 +58,7 @@ from lance_namespace import ( ListTablesRequest, DescribeNamespaceRequest, DropTableRequest, + RenameTableRequest, ListNamespacesRequest, CreateNamespaceRequest, DropNamespaceRequest, @@ -604,9 +605,14 @@ class LanceNamespaceDBConnection(DBConnection): cur_namespace_path = [] if new_namespace_path is None: new_namespace_path = [] - raise NotImplementedError( - "rename_table is not supported for namespace connections" + cur_table_id = cur_namespace_path + [cur_name] + new_namespace_id = new_namespace_path if new_namespace_path else None + request = RenameTableRequest( + id=cur_table_id, + new_table_name=new_name, + new_namespace_id=new_namespace_id, ) + self._namespace_client.rename_table(request) @override def drop_database(self): @@ -1036,14 +1042,19 @@ class AsyncLanceNamespaceDBConnection: cur_namespace_path: Optional[List[str]] = None, new_namespace_path: Optional[List[str]] = None, ): - """Rename is not supported for namespace connections.""" + """Rename a table in the namespace.""" if cur_namespace_path is None: cur_namespace_path = [] if new_namespace_path is None: new_namespace_path = [] - raise NotImplementedError( - "rename_table is not supported for namespace connections" + cur_table_id = cur_namespace_path + [cur_name] + new_namespace_id = new_namespace_path if new_namespace_path else None + request = RenameTableRequest( + id=cur_table_id, + new_table_name=new_name, + new_namespace_id=new_namespace_id, ) + self._namespace_client.rename_table(request) async def drop_database(self): """Deprecated method.""" diff --git a/python/python/tests/test_namespace.py b/python/python/tests/test_namespace.py index f51690365..cd2b3ee44 100644 --- a/python/python/tests/test_namespace.py +++ b/python/python/tests/test_namespace.py @@ -257,8 +257,15 @@ class TestNamespaceConnection: assert table_schema.field("id").type == pa.int64() assert table_schema.field("text").type == pa.string() - def test_rename_table_not_supported(self): - """Test that rename_table raises NotImplementedError.""" + def test_rename_table(self): + """Test that rename_table renames a table in the namespace. + + The `dir` namespace implementation in lance-namespace-impls does not + implement `rename_table` yet (only the `rest` backend does), so it + currently falls back to the default trait method which raises + NotSupported. This is expected to start passing once the `dir` + backend gains rename_table support upstream. + """ db = lancedb.connect_namespace("dir", {"root": self.temp_dir}) # Create a child namespace first @@ -273,9 +280,14 @@ class TestNamespaceConnection: ) db.create_table("old_name", schema=schema, namespace_path=["test_ns"]) - # Rename should raise NotImplementedError - with pytest.raises(NotImplementedError, match="rename_table is not supported"): - db.rename_table("old_name", "new_name") + # Rename the table within the same namespace + with pytest.raises(NotImplementedError, match="rename_table not implemented"): + db.rename_table( + "old_name", + "new_name", + cur_namespace_path=["test_ns"], + new_namespace_path=["test_ns"], + ) def test_drop_all_tables(self): """Test dropping all tables through namespace.""" diff --git a/rust/lancedb/src/database/namespace.rs b/rust/lancedb/src/database/namespace.rs index 1da3c66e2..5ce6a0f55 100644 --- a/rust/lancedb/src/database/namespace.rs +++ b/rust/lancedb/src/database/namespace.rs @@ -16,7 +16,7 @@ use lance_namespace::{ CreateNamespaceRequest, CreateNamespaceResponse, DeclareTableRequest, DescribeNamespaceRequest, DescribeNamespaceResponse, DescribeTableRequest, DropNamespaceRequest, DropNamespaceResponse, DropTableRequest, ListNamespacesRequest, - ListNamespacesResponse, ListTablesRequest, ListTablesResponse, + ListNamespacesResponse, ListTablesRequest, ListTablesResponse, RenameTableRequest, }, }; use lance_namespace_impls::ConnectBuilder; @@ -491,14 +491,34 @@ impl Database for LanceNamespaceDatabase { async fn rename_table( &self, - _cur_name: &str, - _new_name: &str, - _cur_namespace_path: &[String], - _new_namespace_path: &[String], + cur_name: &str, + new_name: &str, + cur_namespace_path: &[String], + new_namespace_path: &[String], ) -> Result<()> { - Err(Error::NotSupported { - message: "rename_table is not supported for namespace connections".to_string(), - }) + let mut cur_table_id = cur_namespace_path.to_vec(); + cur_table_id.push(cur_name.to_string()); + + let new_namespace_id = if new_namespace_path.is_empty() { + None + } else { + Some(new_namespace_path.to_vec()) + }; + + let rename_request = RenameTableRequest { + id: Some(cur_table_id), + new_table_name: new_name.to_string(), + new_namespace_id, + ..Default::default() + }; + self.namespace + .rename_table(rename_request) + .await + .map_err(|e| Error::Runtime { + message: format!("Failed to rename table: {}", e), + })?; + + Ok(()) } async fn drop_table(&self, name: &str, namespace_path: &[String]) -> Result<()> { diff --git a/rust/lancedb/src/table/dataset.rs b/rust/lancedb/src/table/dataset.rs index 7ccca744a..2dd3b7189 100644 --- a/rust/lancedb/src/table/dataset.rs +++ b/rust/lancedb/src/table/dataset.rs @@ -516,10 +516,6 @@ mod tests { let uri = dir.path().to_str().unwrap(); let ds = create_test_dataset(uri).await; - // Other tests use a thread-local mock clock. Simulate leaked state from a - // previous test to ensure this wrapper starts from real time. - clock::advance_by(Duration::from_secs(60)); - let wrapper = DatasetConsistencyWrapper::new_latest(ds, Some(Duration::from_millis(200))); // Populate the cache @@ -529,12 +525,13 @@ mod tests { // External write append_to_dataset(uri).await; - // Should return cached value immediately (within TTL) + // Should return cached value immediately (within TTL), regardless of how + // long the external write above took on a slow CI runner. let v_cached = wrapper.get().await.unwrap().version().version; assert_eq!(v_cached, 1); - // Wait for TTL to expire, then get() should trigger a refresh - tokio::time::sleep(Duration::from_millis(300)).await; + // Advance the mock clock past the TTL so the next get() triggers a refresh. + clock::advance_by(Duration::from_millis(300)); let v_after = wrapper.get().await.unwrap().version().version; assert_eq!(v_after, 2); }