mirror of
https://github.com/lancedb/lancedb.git
synced 2026-11-13 22:26:53 +00:00
fix: separate Function object revisions from OCI digests
This commit is contained in:
@@ -737,10 +737,12 @@ class DBConnection(EnforceOverrides):
|
||||
)
|
||||
|
||||
def drop_function(self, name: str, *, version: str) -> bool:
|
||||
"""Drop one exact immutable Function version from the remote catalog.
|
||||
"""Remove the current Function name binding from the remote catalog.
|
||||
|
||||
Returns True when the version changed to Dropped and False for an
|
||||
idempotent replay. Local connections raise NotImplementedError.
|
||||
The requested version must exist in the currently named object.
|
||||
Object history and existing computed-column references are retained.
|
||||
Returns True when the name was removed and False when it was absent.
|
||||
Local connections raise NotImplementedError.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Function catalog operations are not supported for this connection type"
|
||||
@@ -2297,7 +2299,7 @@ class AsyncConnection(object):
|
||||
]
|
||||
|
||||
async def drop_function(self, name: str, *, version: str) -> bool:
|
||||
"""Drop one exact immutable Function version from the remote catalog."""
|
||||
"""Remove the current name binding, retaining the object and its history."""
|
||||
return await self._inner.drop_function(name, version)
|
||||
|
||||
async def list_jobs(self) -> List[JobInfo]:
|
||||
|
||||
@@ -41,6 +41,7 @@ from typing import (
|
||||
|
||||
import pyarrow as pa
|
||||
from pydantic import (
|
||||
AfterValidator,
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
@@ -303,14 +304,31 @@ class FunctionImage(_RemoteValue):
|
||||
source: bool
|
||||
|
||||
|
||||
def _validate_object_version(value: str) -> str:
|
||||
if int(value) > 2**64 - 1:
|
||||
raise ValueError("Function version exceeds uint64")
|
||||
return value
|
||||
|
||||
|
||||
_ObjectVersion = Annotated[
|
||||
str,
|
||||
Field(strict=True, pattern=r"^[1-9][0-9]*$"),
|
||||
AfterValidator(_validate_object_version),
|
||||
]
|
||||
|
||||
|
||||
class FunctionVersion(_RemoteValue):
|
||||
"""An immutable catalog name bound to a concrete OCI Function image."""
|
||||
"""A pinned object revision, independent of its executable image digest."""
|
||||
|
||||
name: str
|
||||
version: str
|
||||
object_id: str
|
||||
location: str
|
||||
version: _ObjectVersion
|
||||
image: FunctionImage
|
||||
signature: FunctionSignature
|
||||
created_at: str
|
||||
metadata: Mapping[str, str]
|
||||
disabled: bool
|
||||
|
||||
def __call__(self, **inputs: Any) -> FunctionApplication:
|
||||
"""Bind this exact version to named table columns.
|
||||
@@ -364,7 +382,13 @@ class FunctionVersion(_RemoteValue):
|
||||
)
|
||||
)
|
||||
return FunctionApplication(
|
||||
function=FunctionVersionRef(name=self.name, version=self.version),
|
||||
function=FunctionVersionRef(
|
||||
name=self.name,
|
||||
object_id=self.object_id,
|
||||
location=self.location,
|
||||
version=self.version,
|
||||
manifest_digest=self.image.manifest_digest,
|
||||
),
|
||||
inputs=tuple(bindings),
|
||||
output=self.signature.output,
|
||||
)
|
||||
@@ -381,7 +405,10 @@ class FunctionRegistrationRequest(_RemoteValue):
|
||||
|
||||
class FunctionVersionRef(_OpenRemoteValue):
|
||||
name: str
|
||||
version: str
|
||||
object_id: str
|
||||
location: str
|
||||
version: _ObjectVersion
|
||||
manifest_digest: str
|
||||
|
||||
|
||||
class ApplicationInput(_OpenRemoteValue):
|
||||
|
||||
@@ -93,17 +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 == version.image.manifest_digest
|
||||
assert version.version.startswith("sha256:")
|
||||
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.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():
|
||||
@@ -138,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},
|
||||
@@ -223,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
|
||||
@@ -340,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,7 +45,7 @@ FIXTURES = (
|
||||
)
|
||||
|
||||
|
||||
IMAGE_VERSION = json.loads(
|
||||
FUNCTION_VERSION = json.loads(
|
||||
(FIXTURES / "remote_function_version.canonical.json").read_text()
|
||||
)["version"]
|
||||
|
||||
@@ -1204,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=IMAGE_VERSION)
|
||||
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=IMAGE_VERSION)
|
||||
db.drop_function("normalize_score", version=FUNCTION_VERSION)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
@@ -1235,7 +1235,11 @@ def _mock_remote_function_catalog():
|
||||
if self.path == "/v1/function/normalize_score/create":
|
||||
state["version"] = {
|
||||
"name": "normalize_score",
|
||||
"version": IMAGE_VERSION,
|
||||
"version": FUNCTION_VERSION,
|
||||
"object_id": "fixture",
|
||||
"location": "memory:///fixture",
|
||||
"metadata": {},
|
||||
"disabled": False,
|
||||
"image": json.loads(
|
||||
(
|
||||
FIXTURES / "remote_function_version.canonical.json"
|
||||
@@ -1255,10 +1259,10 @@ def _mock_remote_function_catalog():
|
||||
"result": state["version"],
|
||||
}
|
||||
elif self.path == "/v1/function/normalize_score/describe":
|
||||
assert body == {"version": IMAGE_VERSION}
|
||||
assert body == {"version": FUNCTION_VERSION}
|
||||
response = state["version"]
|
||||
elif self.path == "/v1/function/normalize_score/drop":
|
||||
assert body == {"version": IMAGE_VERSION}
|
||||
assert body == {"version": FUNCTION_VERSION}
|
||||
response = {"dropped": True}
|
||||
else:
|
||||
status = 404
|
||||
@@ -1281,7 +1285,7 @@ def _mock_remote_function_catalog():
|
||||
"functions": [
|
||||
{
|
||||
"name": "normalize_score",
|
||||
"version": IMAGE_VERSION,
|
||||
"version": FUNCTION_VERSION,
|
||||
"definition": state["version"],
|
||||
}
|
||||
],
|
||||
@@ -1317,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 == IMAGE_VERSION
|
||||
assert reopened.version == FUNCTION_VERSION
|
||||
create_request = state["requests"][0][1]
|
||||
expected_request = json.loads(
|
||||
normalize_score.registration_request.to_canonical_json()
|
||||
@@ -1337,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 == IMAGE_VERSION
|
||||
assert created.version == FUNCTION_VERSION
|
||||
assert [path for path, _ in state["requests"]] == [
|
||||
"/v1/function/normalize_score/create",
|
||||
"/v1/jobs/describe",
|
||||
@@ -1395,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=IMAGE_VERSION) is True
|
||||
assert db.drop_function("normalize_score", version=FUNCTION_VERSION) is True
|
||||
|
||||
assert state["requests"] == [
|
||||
(
|
||||
"/v1/function/normalize_score/drop",
|
||||
{"version": IMAGE_VERSION},
|
||||
{"version": FUNCTION_VERSION},
|
||||
)
|
||||
]
|
||||
|
||||
@@ -1414,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=IMAGE_VERSION) is True
|
||||
assert (
|
||||
await db.drop_function("normalize_score", version=FUNCTION_VERSION) is True
|
||||
)
|
||||
|
||||
assert state["requests"] == [
|
||||
(
|
||||
"/v1/function/normalize_score/drop",
|
||||
{"version": IMAGE_VERSION},
|
||||
{"version": FUNCTION_VERSION},
|
||||
)
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user