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:
Xuanwo
2026-11-11 10:39:03 +08:00
committed by GitHub
parent 1e5b6deee3
commit c9d176ae7e
13 changed files with 147 additions and 35 deletions
+4 -1
View File
@@ -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,
+29 -5
View File
@@ -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)
)
+3 -2
View File
@@ -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():
+8 -8
View File
@@ -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()
})
}