mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-14 15:52:26 +00:00
feat: represent registered Functions by OCI image identity (#4176)
Function versions identify independent Function objects and their numeric revisions. Rust and Python expose the object ID, location, canonical decimal version, metadata, and availability separately from the OCI image digest. Computed-column applications carry the complete object reference, so existing bindings retain their identity after a name is removed and reused. Source authoring keeps the existing create_function/create_function_async, job wait, and column-binding APIs. The server coordinates baking followed by registration; users do not have to manage manifest digests to create a Function. The previous stored Function representation is intentionally unsupported. Existing contract tests and shared wire fixtures are migrated to the new model. This SDK change accompanies the final integration layer https://github.com/lancedb/sophon/pull/7887 in the Sophon stack: https://github.com/lancedb/sophon/pull/7885 → https://github.com/lancedb/sophon/pull/7886 → https://github.com/lancedb/sophon/pull/7887. A metadata-only revision can retain the same executable image; Function version numbers must not be used as image cache keys. --------- Co-authored-by: lancedb automation <robot@lancedb.com> Co-authored-by: Yang Cen <bubble-cal@outlook.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
lancedb automation
Yang Cen
Claude Fable 5.1
parent
f7579d2aae
commit
3b37ea2c7a
@@ -93,16 +93,22 @@ def test_function_version_identity_is_immutable_and_exact():
|
||||
value = job_result("remote_function_job.json")
|
||||
version = FunctionVersion.from_json(json.dumps(value))
|
||||
assert version.name == "embed"
|
||||
assert version.version == "fv_01K3EXACT"
|
||||
assert version.version == "1"
|
||||
assert version.image.manifest_digest.startswith("sha256:")
|
||||
assert version.version != version.image.manifest_digest
|
||||
|
||||
with pytest.raises((TypeError, ValueError)):
|
||||
version.version = "fv_changed"
|
||||
version.version = "1"
|
||||
with pytest.raises(TypeError, match="immutable"):
|
||||
version.runtime.env["TOKENIZERS_PARALLELISM"] = "true"
|
||||
version.image.descriptor["format_version"] = "changed"
|
||||
|
||||
changed = dict(value)
|
||||
changed["version"] = "fv_changed"
|
||||
changed["version"] = "2"
|
||||
assert FunctionVersion(**changed) != version
|
||||
assert FunctionVersion(**changed).image == version.image
|
||||
for invalid in [version.image.manifest_digest, "0", "01", "-1", str(2**64)]:
|
||||
with pytest.raises(ValueError):
|
||||
FunctionVersion(**{**value, "version": invalid})
|
||||
|
||||
|
||||
def test_function_version_binds_named_columns_as_one_immutable_application():
|
||||
@@ -137,7 +143,7 @@ def test_function_version_binding_validates_names_and_direct_columns():
|
||||
def test_function_version_keeps_named_struct_outputs_in_one_application():
|
||||
value = job_result("remote_function_job.json")
|
||||
value["name"] = "text_features"
|
||||
value["version"] = "fv_multi_output"
|
||||
value["version"] = "1"
|
||||
value["signature"] = {
|
||||
"inputs": [
|
||||
{"name": "title", "arrow_type": "utf8", "nullable": True},
|
||||
@@ -182,14 +188,14 @@ def test_function_version_keeps_named_struct_outputs_in_one_application():
|
||||
def test_unknown_fields_and_discriminators_are_forward_decodable():
|
||||
value = job_result("remote_function_job.json")
|
||||
value["future_version_metadata"] = {"retention_class": "catalog"}
|
||||
value["runtime"] = {"kind": "wasm", "module_digest": "sha256:wasm"}
|
||||
value["image"]["descriptor"]["future_interface"] = {"kind": "wasm"}
|
||||
value["signature"]["output"]["kind"] = "future_output_shape"
|
||||
|
||||
version = FunctionVersion.from_json(json.dumps(value))
|
||||
assert version.runtime.kind == "wasm"
|
||||
assert version.runtime.python_version is None
|
||||
assert version.runtime.environment is None
|
||||
assert json.loads(version.to_canonical_json())["runtime"] == {"kind": "wasm"}
|
||||
assert version.image.descriptor["future_interface"] == {"kind": "wasm"}
|
||||
assert json.loads(version.to_canonical_json())["image"]["descriptor"][
|
||||
"future_interface"
|
||||
] == {"kind": "wasm"}
|
||||
assert version.signature.output.kind == "future_output_shape"
|
||||
|
||||
|
||||
@@ -222,7 +228,7 @@ def test_function_application_uses_rename_columns_only():
|
||||
|
||||
def test_binding_and_refresh_result_keep_stable_remote_fields():
|
||||
binding = FunctionBinding.from_json(fixture("remote_function_binding.json"))
|
||||
assert binding.function.version == "fv_01K3TEXT"
|
||||
assert binding.function.version == "1"
|
||||
assert [output.output_ordinal for output in binding.outputs] == [0, 1]
|
||||
assert binding.input_schema is not None
|
||||
assert binding.output_schema is not None
|
||||
@@ -339,7 +345,16 @@ def test_rename_requires_named_struct_and_keeps_partial_mapping_immutable():
|
||||
scalar = FunctionApplication.from_json(
|
||||
json.dumps(
|
||||
{
|
||||
"function": {"name": "embed", "version": "fv_exact"},
|
||||
"function": {
|
||||
"name": "embed",
|
||||
"version": "1",
|
||||
"object_id": "fixture",
|
||||
"location": "memory:///fixture",
|
||||
"manifest_digest": (
|
||||
"sha256:"
|
||||
"7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"
|
||||
),
|
||||
},
|
||||
"inputs": [],
|
||||
"output": {
|
||||
"kind": "scalar",
|
||||
|
||||
@@ -45,6 +45,11 @@ FIXTURES = (
|
||||
)
|
||||
|
||||
|
||||
FUNCTION_VERSION = json.loads(
|
||||
(FIXTURES / "remote_function_version.canonical.json").read_text()
|
||||
)["version"]
|
||||
|
||||
|
||||
@udf(
|
||||
pip=["numpy>=2"],
|
||||
env={"MODE": "test"},
|
||||
@@ -1199,11 +1204,11 @@ def test_local_function_catalog_operations_are_not_supported(tmp_path):
|
||||
with pytest.raises(NotImplementedError, match=message):
|
||||
db.create_function_async(normalize_score)
|
||||
with pytest.raises(NotImplementedError, match=message):
|
||||
db.get_function("normalize_score", version="fv_exact")
|
||||
db.get_function("normalize_score", version=FUNCTION_VERSION)
|
||||
with pytest.raises(NotImplementedError, match=message):
|
||||
db.list_functions()
|
||||
with pytest.raises(NotImplementedError, match=message):
|
||||
db.drop_function("normalize_score", version="fv_exact")
|
||||
db.drop_function("normalize_score", version=FUNCTION_VERSION)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
@@ -1230,15 +1235,17 @@ def _mock_remote_function_catalog():
|
||||
if self.path == "/v1/function/normalize_score/create":
|
||||
state["version"] = {
|
||||
"name": "normalize_score",
|
||||
"version": "fv_exact",
|
||||
"artifact": {
|
||||
key: body["artifact"][key]
|
||||
for key in ("kind", "digest", "entrypoint")
|
||||
},
|
||||
"version": FUNCTION_VERSION,
|
||||
"object_id": "fixture",
|
||||
"location": "memory:///fixture",
|
||||
"metadata": {},
|
||||
"disabled": False,
|
||||
"image": json.loads(
|
||||
(
|
||||
FIXTURES / "remote_function_version.canonical.json"
|
||||
).read_text()
|
||||
)["image"],
|
||||
"signature": body["signature"],
|
||||
"runtime": body["runtime"],
|
||||
"runtime_digest": "sha256:runtime",
|
||||
"environment_digest": "sha256:environment",
|
||||
"created_at": "2026-08-21T00:00:00Z",
|
||||
}
|
||||
response = {"job_id": "job-register"}
|
||||
@@ -1252,10 +1259,10 @@ def _mock_remote_function_catalog():
|
||||
"result": state["version"],
|
||||
}
|
||||
elif self.path == "/v1/function/normalize_score/describe":
|
||||
assert body == {"version": "fv_exact"}
|
||||
assert body == {"version": FUNCTION_VERSION}
|
||||
response = state["version"]
|
||||
elif self.path == "/v1/function/normalize_score/drop":
|
||||
assert body == {"version": "fv_exact"}
|
||||
assert body == {"version": FUNCTION_VERSION}
|
||||
response = {"dropped": True}
|
||||
else:
|
||||
status = 404
|
||||
@@ -1278,7 +1285,7 @@ def _mock_remote_function_catalog():
|
||||
"functions": [
|
||||
{
|
||||
"name": "normalize_score",
|
||||
"version": "fv_exact",
|
||||
"version": FUNCTION_VERSION,
|
||||
"definition": state["version"],
|
||||
}
|
||||
],
|
||||
@@ -1314,7 +1321,7 @@ def test_remote_registration_job_and_exact_version_reopen_round_trip():
|
||||
|
||||
assert created == reopened
|
||||
assert reopened.name == "normalize_score"
|
||||
assert reopened.version == "fv_exact"
|
||||
assert reopened.version == FUNCTION_VERSION
|
||||
create_request = state["requests"][0][1]
|
||||
expected_request = json.loads(
|
||||
normalize_score.registration_request.to_canonical_json()
|
||||
@@ -1334,7 +1341,7 @@ def test_blocking_remote_registration_returns_function_version():
|
||||
created = db.create_function(normalize_score)
|
||||
|
||||
assert created.name == "normalize_score"
|
||||
assert created.version == "fv_exact"
|
||||
assert created.version == FUNCTION_VERSION
|
||||
assert [path for path, _ in state["requests"]] == [
|
||||
"/v1/function/normalize_score/create",
|
||||
"/v1/jobs/describe",
|
||||
@@ -1392,12 +1399,12 @@ def test_remote_drop_function_sends_exact_version():
|
||||
host_override=host,
|
||||
client_config={"retry_config": {"retries": 0}},
|
||||
)
|
||||
assert db.drop_function("normalize_score", version="fv_exact") is True
|
||||
assert db.drop_function("normalize_score", version=FUNCTION_VERSION) is True
|
||||
|
||||
assert state["requests"] == [
|
||||
(
|
||||
"/v1/function/normalize_score/drop",
|
||||
{"version": "fv_exact"},
|
||||
{"version": FUNCTION_VERSION},
|
||||
)
|
||||
]
|
||||
|
||||
@@ -1411,11 +1418,13 @@ async def test_async_remote_drop_function_sends_exact_version():
|
||||
host_override=host,
|
||||
client_config={"retry_config": {"retries": 0}},
|
||||
)
|
||||
assert await db.drop_function("normalize_score", version="fv_exact") is True
|
||||
assert (
|
||||
await db.drop_function("normalize_score", version=FUNCTION_VERSION) is True
|
||||
)
|
||||
|
||||
assert state["requests"] == [
|
||||
(
|
||||
"/v1/function/normalize_score/drop",
|
||||
{"version": "fv_exact"},
|
||||
{"version": FUNCTION_VERSION},
|
||||
)
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user