feat: record the namespace a view's unqualified names resolve against

A view outlives the session that declared it, so the namespace path its
query resolved against travels with it, beside the database. Without it
a reader would resolve an unqualified name against its own default and
read a different table.

`ViewDescription` carries `default_namespace_path`; it crosses the wire
as `default_namespace`, a path like `namespace`, absent for the root.
This commit is contained in:
Jack Ye
2026-09-22 10:14:49 -07:00
parent 24628a8bb9
commit 151d507f3d
12 changed files with 70 additions and 8 deletions
+2 -2
View File
@@ -170,10 +170,10 @@ class Connection(object):
) -> Tuple[str, int, int]: ...
async def create_view(
self, name: str, query: str, namespace_path: Optional[List[str]] = None
) -> Tuple[str, List[str], str, str, pa.Schema]: ...
) -> Tuple[str, List[str], str, str, List[str], pa.Schema]: ...
async def describe_view(
self, name: str, namespace_path: Optional[List[str]] = None
) -> Tuple[str, List[str], str, str, pa.Schema]: ...
) -> Tuple[str, List[str], str, str, List[str], pa.Schema]: ...
async def drop_view(
self, name: str, namespace_path: Optional[List[str]] = None
) -> None: ...
+10 -2
View File
@@ -98,16 +98,24 @@ from .namespace_utils import (
def _view_description(
described: Tuple[str, List[str], str, str, "pa.Schema"],
described: Tuple[str, List[str], str, str, List[str], "pa.Schema"],
) -> ViewDescription:
"""Name the fields the binding returns positionally."""
name, namespace_path, query, default_database, schema = described
(
name,
namespace_path,
query,
default_database,
default_namespace_path,
schema,
) = described
return ViewDescription(
name=name,
query=query,
default_database=default_database,
schema=schema,
namespace_path=namespace_path,
default_namespace_path=default_namespace_path,
)
+7
View File
@@ -31,3 +31,10 @@ class ViewDescription:
"""The schema the defining query resolved to when the view was created."""
namespace_path: List[str] = field(default_factory=list)
"""The namespace holding the view; empty is the root namespace."""
default_namespace_path: List[str] = field(default_factory=list)
"""The namespace path those unqualified names resolve against.
Recorded with the view because it outlives the session that declared it: a
reader resolving the query against its own default namespace could read a
different table than the view was defined over.
"""
+2
View File
@@ -2766,6 +2766,7 @@ def test_view_crud_addresses_its_own_routes():
"namespace": ["analytics"],
"query": "SELECT name FROM people",
"default_database": "dev",
"default_namespace": ["analytics"],
"schema": {
"fields": [
{"name": "name", "nullable": True, "type": {"type": "utf8"}}
@@ -2785,6 +2786,7 @@ def test_view_crud_addresses_its_own_routes():
assert view.namespace_path == ["analytics"]
assert view.query == "SELECT name FROM people"
assert view.default_database == "dev"
assert view.default_namespace_path == ["analytics"]
assert view.schema == pa.schema([pa.field("name", pa.utf8(), nullable=True)])
described = db.describe_view("adults", namespace_path=["analytics"])
+2 -1
View File
@@ -106,7 +106,7 @@ fn parse_default_namespace_path(path: Option<Bound<'_, PyAny>>) -> PyResult<Vec<
/// A view description on its way to Python: name, namespace, query, default
/// database, and the schema as pyarrow renders it.
type PyViewDescription = (String, Vec<String>, String, String, Py<PyAny>);
type PyViewDescription = (String, Vec<String>, String, String, Vec<String>, Py<PyAny>);
/// A view description as a plain tuple, with the schema converted to the
/// pyarrow schema the caller would get from any other lancedb API. The Python
@@ -120,6 +120,7 @@ fn view_description_to_py(view: lancedb::view::ViewDescription) -> PyResult<PyVi
view.namespace_path,
view.query,
view.default_database,
view.default_namespace_path,
schema,
))
})