From 5eaac178b18310633a7d474dfccc3bbe19cf59dd Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Fri, 17 Apr 2026 01:11:18 -0700 Subject: [PATCH] fix(python): pass namespace client on schema-only table create (#3283) ## Summary - pass `namespace_client` through the Python create-table path - ensure schema-only namespace table creation uses the namespace-aware empty-table flow - fix reopening namespace tables created without initial data --- python/python/lancedb/db.py | 3 + python/python/lancedb/table.py | 1 + .../tests/test_namespace_integration.py | 63 +++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/python/python/lancedb/db.py b/python/python/lancedb/db.py index d62b87f5d..b07d409eb 100644 --- a/python/python/lancedb/db.py +++ b/python/python/lancedb/db.py @@ -1390,6 +1390,7 @@ class AsyncConnection(object): namespace_path: Optional[List[str]] = None, embedding_functions: Optional[List[EmbeddingFunctionConfig]] = None, location: Optional[str] = None, + namespace_client: Optional[Any] = None, ) -> AsyncTable: """Create an [AsyncTable][lancedb.table.AsyncTable] in the database. @@ -1587,6 +1588,7 @@ class AsyncConnection(object): namespace_path=namespace_path, storage_options=storage_options, location=location, + namespace_client=namespace_client, ) else: data = data_to_reader(data, schema) @@ -1597,6 +1599,7 @@ class AsyncConnection(object): namespace_path=namespace_path, storage_options=storage_options, location=location, + namespace_client=namespace_client, ) return AsyncTable(new_table) diff --git a/python/python/lancedb/table.py b/python/python/lancedb/table.py index 3213696f6..204e9c5b7 100644 --- a/python/python/lancedb/table.py +++ b/python/python/lancedb/table.py @@ -2929,6 +2929,7 @@ class LanceTable(Table): namespace_path=namespace_path, storage_options=storage_options, location=location, + namespace_client=namespace_client, ) ) return self diff --git a/python/python/tests/test_namespace_integration.py b/python/python/tests/test_namespace_integration.py index 01cc4a9de..84455c979 100644 --- a/python/python/tests/test_namespace_integration.py +++ b/python/python/tests/test_namespace_integration.py @@ -18,6 +18,9 @@ Tests verify: """ import copy +import shutil +import sys +import tempfile import time import uuid from typing import Dict, Optional @@ -387,6 +390,66 @@ def test_namespace_open_table_with_provider(s3_bucket: str, use_custom: bool): assert get_describe_call_count(inner_ns_client) == describe_count_after_open +@pytest.mark.skipif( + sys.platform == "win32", + reason="TODO: fix schema-only namespace metrics test on Windows", +) +@pytest.mark.parametrize("use_custom", [False, True], ids=["DirectoryNS", "CustomNS"]) +def test_namespace_create_schema_only_with_provider(use_custom: bool): + """ + Test creating a schema-only table through namespace with storage options provider. + + Verifies: + - declare_table is called once to reserve the location + - describe_table is not needed during create in create mode + - the table can be reopened successfully afterward + - opening the table triggers exactly one describe_table call + """ + temp_dir = tempfile.mkdtemp() + try: + ns_client, inner_ns_client = create_tracking_namespace( + bucket_name=temp_dir, + storage_options={}, + credential_expires_in_seconds=3600, + use_custom=use_custom, + ) + + db = LanceNamespaceDBConnection(ns_client) + + namespace_name = f"test_ns_{uuid.uuid4().hex[:8]}" + db.create_namespace([namespace_name]) + + table_name = f"test_table_{uuid.uuid4().hex}" + namespace_path = [namespace_name] + schema = pa.schema( + [ + pa.field("id", pa.int64()), + pa.field("vector", pa.list_(pa.float32(), 2)), + pa.field("text", pa.string()), + ] + ) + + assert get_declare_call_count(inner_ns_client) == 0 + assert get_describe_call_count(inner_ns_client) == 0 + + table = db.create_table( + table_name, schema=schema, namespace_path=namespace_path + ) + + assert table.name == table_name + assert table.namespace == namespace_path + assert get_declare_call_count(inner_ns_client) == 1 + assert get_describe_call_count(inner_ns_client) == 0 + + reopened_table = db.open_table(table_name, namespace_path=namespace_path) + + assert reopened_table.schema == schema + assert get_declare_call_count(inner_ns_client) == 1 + assert get_describe_call_count(inner_ns_client) == 1 + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + @pytest.mark.s3_test @pytest.mark.parametrize("use_custom", [False, True], ids=["DirectoryNS", "CustomNS"]) def test_namespace_credential_refresh_on_read(s3_bucket: str, use_custom: bool):