mirror of
https://github.com/lancedb/lancedb.git
synced 2026-11-13 14:16:54 +00:00
feat: resolve latest Function versions (#4949)
## Problem Function catalog lookup required an exact version string even though callers commonly need the latest currently visible immutable revision. On the current SDK, resolving that version required an out-of-band catalog query before calling `get_function`. ## Change - Make Python `get_function(name)` default to latest while accepting `int` or `str` for exact lookup. - Normalize integer versions to their decimal string form and reject `bool` explicitly. Existing string validation and errors remain unchanged. - Thread `None` through the rebuilt PyO3 binding and route it to the new Rust `Connection::get_function_latest`. - Resolve latest through the existing catalog `DESCRIBE FUNCTION` path without `WITH (version = ...)`. The returned value is one exact immutable version, not a mutable alias. - Preserve the existing Rust exact-version API. ## Validation On the designated EC2 host with the rebuilt extension: - `test_local_function_catalog_operations_are_not_supported`: **1 passed**. This crosses the Python connection, rebuilt PyO3, and embedded Rust `NotSupported` boundary with the omitted-version call. - Async and sync catalog-path tests: **2 passed**, 117 deselected. - Remote SQL Function creation/description integration test: **1 passed**. - An earlier Rust invocation with the wrong module path matched **0 tests**; it is retained as harness evidence and is not counted as a pass. ## Scope This is the Function lookup SDK slice. It does not include the separate atomic rebind/publication server work tracked under ENT-3418.
This commit is contained in:
@@ -190,7 +190,10 @@ class Connection(object):
|
||||
self, request_json: str, namespace_path: Optional[List[str]] = None
|
||||
) -> Job: ...
|
||||
async def get_function(
|
||||
self, name: str, version: str, namespace_path: Optional[List[str]] = None
|
||||
self,
|
||||
name: str,
|
||||
version: Optional[str] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> str: ...
|
||||
def list_functions(
|
||||
self,
|
||||
|
||||
@@ -140,6 +140,18 @@ def _view_description(
|
||||
)
|
||||
|
||||
|
||||
def _normalize_function_version(version: Optional[Union[int, str]]) -> Optional[str]:
|
||||
if version is None:
|
||||
return None
|
||||
if isinstance(version, bool):
|
||||
raise TypeError("version must be an int, str, or None")
|
||||
if isinstance(version, int):
|
||||
return str(version)
|
||||
if isinstance(version, str):
|
||||
return version
|
||||
raise TypeError("version must be an int, str, or None")
|
||||
|
||||
|
||||
class DBConnection(EnforceOverrides):
|
||||
"""An active LanceDB connection interface.
|
||||
|
||||
@@ -1027,10 +1039,15 @@ class DBConnection(EnforceOverrides):
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
version: Optional[Union[int, str]] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> FunctionVersion:
|
||||
"""Open one exact immutable Function version from the remote catalog."""
|
||||
"""Open an immutable Function version from the remote catalog.
|
||||
|
||||
``version`` may be an integer or string. When omitted, the server
|
||||
resolves the latest currently visible revision and returns that exact
|
||||
immutable version.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
_append_doc_url(
|
||||
"Function catalog operations are not supported "
|
||||
@@ -2310,9 +2327,10 @@ class LanceDBConnection(DBConnection):
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
version: Optional[Union[int, str]] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> FunctionVersion:
|
||||
version = _normalize_function_version(version)
|
||||
return LOOP.run(
|
||||
self._conn.get_function(
|
||||
name, version=version, namespace_path=namespace_path
|
||||
@@ -3610,10 +3628,16 @@ class AsyncConnection(object):
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
version: Optional[Union[int, str]] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> FunctionVersion:
|
||||
"""Open one exact immutable Function version from the remote catalog."""
|
||||
"""Open an immutable Function version from the remote catalog.
|
||||
|
||||
``version`` may be an integer or string. When omitted, the server
|
||||
resolves the latest currently visible revision and returns that exact
|
||||
immutable version.
|
||||
"""
|
||||
version = _normalize_function_version(version)
|
||||
return FunctionVersion.from_json(
|
||||
await self._inner.get_function(name, version, namespace_path)
|
||||
)
|
||||
|
||||
@@ -37,7 +37,7 @@ import pyarrow as pa
|
||||
|
||||
from ..common import DATA
|
||||
from lancedb.listing import Listing, NamespaceListing, TableListing
|
||||
from ..db import DBConnection, LOOP
|
||||
from ..db import DBConnection, LOOP, _normalize_function_version
|
||||
from ..functions import FunctionVersion, FunctionDefinition
|
||||
from ..graph import EdgeTable, NodeTable, PropertyGraph
|
||||
from ..job import AsyncJob, Job
|
||||
@@ -985,9 +985,10 @@ class RemoteDBConnection(DBConnection):
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str,
|
||||
version: Optional[Union[int, str]] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> FunctionVersion:
|
||||
version = _normalize_function_version(version)
|
||||
return LOOP.run(
|
||||
self._conn.get_function(
|
||||
name, version=version, namespace_path=namespace_path
|
||||
|
||||
@@ -1894,6 +1894,8 @@ def test_local_function_catalog_operations_are_not_supported(tmp_path):
|
||||
db.create_function_async(normalize_score)
|
||||
with pytest.raises(NotImplementedError, match=message):
|
||||
db.get_function("normalize_score", version=FUNCTION_VERSION)
|
||||
with pytest.raises(NotImplementedError, match=message):
|
||||
db.get_function("normalize_score")
|
||||
with pytest.raises(NotImplementedError, match=message):
|
||||
list(db.list_functions())
|
||||
with pytest.raises(NotImplementedError, match=message):
|
||||
@@ -1903,8 +1905,10 @@ def test_local_function_catalog_operations_are_not_supported(tmp_path):
|
||||
class _RawFunctionCatalog:
|
||||
def __init__(self, definition, *, encode=False):
|
||||
self.definition = json.dumps(definition) if encode else definition
|
||||
self.requested_versions = []
|
||||
|
||||
async def get_function(self, _name, version, namespace_path):
|
||||
async def get_function(self, _name, version=None, namespace_path=None):
|
||||
self.requested_versions.append(version)
|
||||
return self.definition
|
||||
|
||||
def list_functions(self, *, namespace_path=None, page_token=None, page_limit=None):
|
||||
@@ -1944,13 +1948,22 @@ async def test_async_catalog_path_preserves_execution_controls():
|
||||
row_timeout_seconds=8.0,
|
||||
result_stability="input_and_context",
|
||||
)
|
||||
connection = AsyncConnection(_RawFunctionCatalog(definition, encode=True))
|
||||
catalog = _RawFunctionCatalog(definition, encode=True)
|
||||
connection = AsyncConnection(catalog)
|
||||
|
||||
latest = await connection.get_function("embed")
|
||||
integer = await connection.get_function("embed", version=1)
|
||||
exact = await connection.get_function("embed", version="1")
|
||||
await connection.get_function("embed", version="001")
|
||||
listed = [item async for item in connection.list_functions(page_limit=32)]
|
||||
assert len(listed) == 1
|
||||
for version in [exact, *listed]:
|
||||
for version in [latest, integer, exact, *listed]:
|
||||
assert _assert_execution_control_encoding(version) == definition
|
||||
assert catalog.requested_versions == [None, "1", "1", "001"]
|
||||
with pytest.raises(TypeError, match="int, str, or None"):
|
||||
await connection.get_function("embed", version=True)
|
||||
with pytest.raises(TypeError, match="int, str, or None"):
|
||||
await connection.get_function("embed", version=1.5)
|
||||
|
||||
|
||||
def test_sync_catalog_path_preserves_execution_controls():
|
||||
@@ -1963,13 +1976,22 @@ def test_sync_catalog_path_preserves_execution_controls():
|
||||
result_stability="input_and_context",
|
||||
)
|
||||
connection = object.__new__(RemoteDBConnection)
|
||||
connection._conn = _RawFunctionCatalog(FunctionVersion(**definition))
|
||||
catalog = _RawFunctionCatalog(FunctionVersion(**definition))
|
||||
connection._conn = catalog
|
||||
|
||||
latest = connection.get_function("embed")
|
||||
integer = connection.get_function("embed", version=1)
|
||||
exact = connection.get_function("embed", version="1")
|
||||
connection.get_function("embed", version="001")
|
||||
listed = list(connection.list_functions(page_limit=32))
|
||||
assert len(listed) == 1
|
||||
for version in [exact, *listed]:
|
||||
for version in [latest, integer, exact, *listed]:
|
||||
assert _assert_execution_control_encoding(version) == definition
|
||||
assert catalog.requested_versions == [None, "1", "1", "001"]
|
||||
with pytest.raises(TypeError, match="int, str, or None"):
|
||||
connection.get_function("embed", version=True)
|
||||
with pytest.raises(TypeError, match="int, str, or None"):
|
||||
connection.get_function("embed", version=1.5)
|
||||
|
||||
|
||||
def test_sync_drop_function_emits_server_warnings_without_changing_bool_api():
|
||||
|
||||
@@ -877,22 +877,22 @@ impl Connection {
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3(signature = (name, version, namespace_path=None))]
|
||||
#[pyo3(signature = (name, version=None, namespace_path=None))]
|
||||
pub fn get_function(
|
||||
self_: PyRef<'_, Self>,
|
||||
name: String,
|
||||
version: String,
|
||||
version: Option<String>,
|
||||
namespace_path: Option<Vec<String>>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let inner = self_.get_inner()?.clone();
|
||||
let namespace_path = namespace_path.unwrap_or_default();
|
||||
future_into_py(self_.py(), async move {
|
||||
inner
|
||||
.get_function(name, version, &namespace_path)
|
||||
.await
|
||||
.infer_error()?
|
||||
.to_canonical_json()
|
||||
.infer_error()
|
||||
let function = match version {
|
||||
Some(version) => inner.get_function(name, version, &namespace_path).await,
|
||||
None => inner.get_function_latest(name, &namespace_path).await,
|
||||
}
|
||||
.infer_error()?;
|
||||
function.to_canonical_json().infer_error()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user