feat: add list_functions client APIs (#4108)

Function registration and exact lookup are exposed through the SDK, but
clients cannot discover published versions even though the server
provides `POST /v1/functions/list`.

Add Rust and Python sync/async `list_functions()` APIs that return typed
`FunctionVersion` values. The remote client requests canonical
definitions and follows opaque page tokens until the listing is
complete, including empty intermediate pages, while preserving the
server's name/version ordering. Local databases retain the existing
Function-catalog unsupported error.

The SDK consumes protocol pagination internally so callers receive the
complete catalog rather than handling server-specific page tokens.
This commit is contained in:
Xuanwo
2026-09-01 23:50:57 +08:00
committed by GitHub
parent 7ebd3c222d
commit 193c5e3458
8 changed files with 302 additions and 1 deletions
@@ -1017,6 +1017,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="fv_exact")
with pytest.raises(NotImplementedError, match=message):
db.list_functions()
with pytest.raises(NotImplementedError, match=message):
db.drop_function("normalize_score", version="fv_exact")
@@ -1064,6 +1066,22 @@ def _mock_remote_function_catalog():
"version": "fv_exact",
}
response = state["version"]
elif self.path == "/v1/functions/list":
assert body["include_definition"] is True
if "page_token" not in body:
response = {
"functions": [
{
"name": "normalize_score",
"version": "fv_exact",
"definition": state["version"],
}
],
"page_token": "next",
}
else:
assert body["page_token"] == "next"
response = {"functions": []}
elif self.path == "/v1/functions/drop":
assert body == {
"name": "normalize_score",
@@ -1130,6 +1148,49 @@ def test_blocking_remote_registration_returns_function_version():
]
def test_remote_list_functions_paginates_and_returns_typed_versions():
with _mock_remote_function_catalog() as (host, state):
db = lancedb.connect(
"db://dev",
api_key="fake",
host_override=host,
client_config={"retry_config": {"retries": 0}},
)
created = db.create_function(normalize_score)
state["requests"].clear()
functions = db.list_functions()
assert functions == [created]
assert state["requests"] == [
("/v1/functions/list", {"include_definition": True}),
(
"/v1/functions/list",
{"include_definition": True, "page_token": "next"},
),
]
@pytest.mark.asyncio
async def test_async_remote_list_functions_returns_typed_versions():
with _mock_remote_function_catalog() as (host, state):
db = await lancedb.connect_async(
"db://dev",
api_key="fake",
host_override=host,
client_config={"retry_config": {"retries": 0}},
)
registration = await db.create_function_async(normalize_score)
created = await registration.wait()
state["requests"].clear()
functions = await db.list_functions()
assert functions == [created]
assert [path for path, _ in state["requests"]] == [
"/v1/functions/list",
"/v1/functions/list",
]
def test_remote_drop_function_sends_exact_version():
with _mock_remote_function_catalog() as (host, state):
db = lancedb.connect(