diff --git a/python/python/lancedb/db.py b/python/python/lancedb/db.py index 5ec697d4e..0db9003d1 100644 --- a/python/python/lancedb/db.py +++ b/python/python/lancedb/db.py @@ -41,6 +41,7 @@ from lance_namespace import ( ListTablesResponse, connect as namespace_connect, ) +from lance_namespace.errors import NamespaceNotEmptyError, TableNotFoundError from . import __version__ from ._lancedb import connect as lancedb_connect # type: ignore @@ -746,10 +747,12 @@ class LanceDBConnection(DBConnection): """ if namespace_path is None: namespace_path = [] - return self._namespace_conn().list_namespaces( - namespace_path=namespace_path, - page_token=page_token, - limit=limit, + return LOOP.run( + self._conn.list_namespaces( + namespace_path=namespace_path, + page_token=page_token, + limit=limit, + ) ) @override @@ -759,10 +762,12 @@ class LanceDBConnection(DBConnection): mode: Optional[str] = None, properties: Optional[Dict[str, str]] = None, ) -> CreateNamespaceResponse: - return self._namespace_conn().create_namespace( - namespace_path=namespace_path, - mode=mode, - properties=properties, + return LOOP.run( + self._conn.create_namespace( + namespace_path=namespace_path, + mode=mode, + properties=properties, + ) ) @override @@ -772,19 +777,24 @@ class LanceDBConnection(DBConnection): mode: Optional[str] = None, behavior: Optional[str] = None, ) -> DropNamespaceResponse: - return self._namespace_conn().drop_namespace( - namespace_path=namespace_path, - mode=mode, - behavior=behavior, - ) + try: + return LOOP.run( + self._conn.drop_namespace( + namespace_path=namespace_path, + mode=mode, + behavior=behavior, + ) + ) + except RuntimeError as e: + if "Namespace not empty" in str(e): + raise NamespaceNotEmptyError(str(e)) from e + raise @override def describe_namespace( self, namespace_path: List[str] ) -> DescribeNamespaceResponse: - return self._namespace_conn().describe_namespace( - namespace_path=namespace_path, - ) + return LOOP.run(self._conn.describe_namespace(namespace_path=namespace_path)) @override def list_tables( @@ -813,12 +823,6 @@ class LanceDBConnection(DBConnection): """ if namespace_path is None: namespace_path = [] - if namespace_path: - return self._namespace_conn().list_tables( - namespace_path=namespace_path, - page_token=page_token, - limit=limit, - ) return LOOP.run( self._conn.list_tables( namespace_path=namespace_path, page_token=page_token, limit=limit @@ -916,22 +920,6 @@ class LanceDBConnection(DBConnection): raise ValueError("mode must be either 'create' or 'overwrite'") validate_table_name(name) - if namespace_path: - return self._namespace_conn().create_table( - name, - data=data, - schema=schema, - mode=mode, - exist_ok=exist_ok, - on_bad_vectors=on_bad_vectors, - fill_value=fill_value, - embedding_functions=embedding_functions, - namespace_path=namespace_path, - storage_options=storage_options, - data_storage_version=data_storage_version, - enable_v2_manifest_paths=enable_v2_manifest_paths, - ) - tbl = LanceTable.create( self, name, @@ -944,22 +932,11 @@ class LanceDBConnection(DBConnection): embedding_functions=embedding_functions, namespace_path=namespace_path, storage_options=storage_options, + data_storage_version=data_storage_version, + enable_v2_manifest_paths=enable_v2_manifest_paths, ) return tbl - def _namespace_conn(self) -> DBConnection: - """Return a LanceNamespaceDBConnection backed by this connection's - directory namespace. Used to delegate child-namespace operations.""" - from lancedb.namespace import LanceNamespaceDBConnection - - return LanceNamespaceDBConnection( - self.namespace_client(), - read_consistency_interval=self.read_consistency_interval, - storage_options=self.storage_options, - namespace_client_impl=None, - namespace_client_properties=None, - ) - @override def open_table( self, @@ -1006,14 +983,7 @@ class LanceDBConnection(DBConnection): stacklevel=2, ) - if namespace_path: - tbl = self._namespace_conn().open_table( - name, - namespace_path=namespace_path, - storage_options=storage_options, - index_cache_size=index_cache_size, - ) - else: + try: tbl = LanceTable.open( self, name, @@ -1021,6 +991,15 @@ class LanceDBConnection(DBConnection): storage_options=storage_options, index_cache_size=index_cache_size, ) + except (RuntimeError, ValueError) as e: + if namespace_path and ( + "Table not found" in str(e) or "was not found" in str(e) + ): + table_id = namespace_path + [name] + raise TableNotFoundError( + f"Table not found: {'$'.join(table_id)}" + ) from e + raise if branch is not None: tbl = tbl.branches.checkout(branch, version) @@ -1104,9 +1083,6 @@ class LanceDBConnection(DBConnection): """ if namespace_path is None: namespace_path = [] - if namespace_path: - self._namespace_conn().drop_table(name, namespace_path=namespace_path) - return LOOP.run( self._conn.drop_table( name, namespace_path=namespace_path, ignore_missing=ignore_missing diff --git a/python/python/lancedb/table.py b/python/python/lancedb/table.py index 4570c9788..20ad2e742 100644 --- a/python/python/lancedb/table.py +++ b/python/python/lancedb/table.py @@ -2195,7 +2195,7 @@ class LanceTable(Table): namespace_client = self._namespace_client if namespace_client is None: conn_uri = getattr(self._conn, "uri", "") - if get_uri_scheme(conn_uri) == "namespace": + if get_uri_scheme(conn_uri) == "namespace" or self._namespace_path: namespace_client = self._conn.namespace_client() self._namespace_client = namespace_client diff --git a/python/python/tests/test_db.py b/python/python/tests/test_db.py index 9495fb330..2bc4a0611 100644 --- a/python/python/tests/test_db.py +++ b/python/python/tests/test_db.py @@ -13,6 +13,7 @@ import numpy as np import pandas as pd import pyarrow as pa import pytest +from lance_namespace.errors import NamespaceNotEmptyError, TableNotFoundError from lancedb.pydantic import LanceModel, Vector @@ -955,6 +956,47 @@ def test_local_namespace_operations(tmp_path): assert db.list_namespaces().namespaces == [] +def test_local_sync_namespace_uses_rust_without_python_client(tmp_path, monkeypatch): + """Sync local namespace operations should avoid the Python namespace client.""" + db = lancedb.connect(tmp_path) + + def fail_namespace_client(): + raise AssertionError("Python namespace client should not be constructed") + + monkeypatch.setattr(db, "namespace_client", fail_namespace_client) + + db.create_namespace(["child"]) + assert "child" in db.list_namespaces().namespaces + + schema = pa.schema([pa.field("id", pa.int64())]) + table = db.create_table("tbl", schema=schema, namespace_path=["child"]) + assert table.namespace == ["child"] + assert "tbl" in db.table_names(namespace_path=["child"]) + assert db.list_tables(namespace_path=["child"]).tables == ["tbl"] + + opened = db.open_table("tbl", namespace_path=["child"]) + assert opened.namespace == ["child"] + + db.drop_table("tbl", namespace_path=["child"]) + assert db.list_tables(namespace_path=["child"]).tables == [] + db.drop_namespace(["child"]) + assert db.list_namespaces().namespaces == [] + + +def test_local_sync_namespace_preserves_public_errors(tmp_path): + db = lancedb.connect(tmp_path) + db.create_namespace(["child"]) + db.create_table( + "tbl", schema=pa.schema([pa.field("id", pa.int64())]), namespace_path=["child"] + ) + + with pytest.raises(TableNotFoundError, match="child\\$missing"): + db.open_table("missing", namespace_path=["child"]) + + with pytest.raises(NamespaceNotEmptyError): + db.drop_namespace(["child"]) + + def test_create_namespace_invalid_mode_raises(tmp_path): """Unrecognized create namespace modes raise a clear error.""" db = lancedb.connect(tmp_path)