fix(python): route local sync namespace operations through rust (#3606)

Routes local sync child-namespace operations through the Rust-backed
connection instead of the Python namespace-client fallback.

Also keeps lazy namespace-client construction for table-to-Lance
conversion and preserves public namespace error mappings.

Validated locally with ruff format/check and targeted namespace pytest.
This commit is contained in:
Jack Ye
2026-07-16 10:53:49 -07:00
committed by GitHub
parent bc8674ab22
commit ca39258342
3 changed files with 81 additions and 63 deletions
+38 -62
View File
@@ -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
+1 -1
View File
@@ -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
+42
View File
@@ -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)