fix: separate Function object revisions from OCI digests

This commit is contained in:
Xuanwo
2026-09-15 15:48:38 +08:00
parent df661cdab0
commit 1568ab941e
23 changed files with 193 additions and 111 deletions
+6 -4
View File
@@ -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]:
+31 -4
View File
@@ -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},
)
]
+1 -1
View File
@@ -631,7 +631,7 @@ impl Connection {
self.internal.list_functions().await
}
/// Drop one exact immutable Function version from the remote catalog.
/// Remove the current Function name binding, retaining the object history.
///
/// Returns `true` when the server appended a Dropped transition and
/// `false` for an idempotent replay. Local databases return
+1 -1
View File
@@ -313,7 +313,7 @@ pub trait Database:
async fn list_functions(&self) -> Result<Vec<crate::function::FunctionVersion>> {
function_catalog_not_supported()
}
/// Drop one exact immutable Function version from the remote catalog.
/// Remove the current Function name binding, retaining the object history.
async fn drop_function(&self, _name: &str, _version: &str) -> Result<bool> {
function_catalog_not_supported()
}
+55 -5
View File
@@ -88,10 +88,18 @@ fn application_has_unknown_nested_fields(value: &Value) -> bool {
let Some(application) = value.as_object() else {
return false;
};
if application
.get("function")
.is_some_and(|value| has_unknown_keys(value, &["name", "version"]))
{
if application.get("function").is_some_and(|value| {
has_unknown_keys(
value,
&[
"name",
"object_id",
"location",
"version",
"manifest_digest",
],
)
}) {
return true;
}
if application
@@ -404,17 +412,55 @@ pub struct FunctionImage {
pub source: bool,
}
/// An immutable catalog name bound to a concrete OCI manifest.
fn deserialize_object_version<'de, D: Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<String, D::Error> {
let value = String::deserialize(deserializer)?;
match value.parse::<u64>() {
Ok(number) if number > 0 && number.to_string() == value => Ok(value),
_ => Err(de::Error::custom(
"Function version must be a canonical positive uint64",
)),
}
}
/// One immutable Function object revision and its executable artifact.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionVersion {
name: String,
object_id: String,
location: String,
#[serde(deserialize_with = "deserialize_object_version")]
version: String,
image: FunctionImage,
signature: FunctionSignature,
created_at: String,
metadata: BTreeMap<String, String>,
disabled: bool,
}
impl FunctionVersion {
pub fn object_id(&self) -> &str {
&self.object_id
}
pub fn location(&self) -> &str {
&self.location
}
pub fn metadata(&self) -> &BTreeMap<String, String> {
&self.metadata
}
pub fn disabled(&self) -> bool {
self.disabled
}
pub fn reference(&self) -> FunctionVersionRef {
FunctionVersionRef {
name: self.name.clone(),
object_id: self.object_id.clone(),
location: self.location.clone(),
version: self.version.clone(),
manifest_digest: self.image.manifest_digest.clone(),
}
}
pub fn name(&self) -> &str {
&self.name
}
@@ -482,7 +528,11 @@ impl_json!(FunctionRegistrationRequest);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionVersionRef {
pub name: String,
pub object_id: String,
pub location: String,
#[serde(deserialize_with = "deserialize_object_version")]
pub version: String,
pub manifest_digest: String,
}
/// Parameter binding in a FunctionApplication.
+8 -37
View File
@@ -2864,10 +2864,7 @@ mod tests {
assert_eq!(job.id(), Some("job-function-1"));
let version = job.wait().await.unwrap();
assert_eq!(version.name(), "embed");
assert_eq!(
version.version(),
"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"
);
assert_eq!(version.version(), "1");
}
#[tokio::test]
@@ -2880,24 +2877,12 @@ mod tests {
assert_eq!(request.url().path(), "/v1/function/embed/describe");
let body: serde_json::Value =
serde_json::from_slice(request.body().unwrap().as_bytes().unwrap()).unwrap();
assert_eq!(
body,
serde_json::json!({"version": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"})
);
assert_eq!(body, serde_json::json!({"version": "1"}));
http::Response::builder().status(200).body(VERSION).unwrap()
});
let version = conn
.get_function(
"embed",
"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6",
)
.await
.unwrap();
let version = conn.get_function("embed", "1").await.unwrap();
assert_eq!(version.name(), "embed");
assert_eq!(
version.version(),
"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"
);
assert_eq!(version.version(), "1");
}
#[tokio::test]
@@ -2928,7 +2913,7 @@ mod tests {
serde_json::json!({
"functions": [{
"name": "embed",
"version": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6",
"version": "1",
"definition": version.clone(),
}],
})
@@ -2941,10 +2926,7 @@ mod tests {
let functions = conn.list_functions().await.unwrap();
assert_eq!(functions.len(), 1);
assert_eq!(functions[0].name(), "embed");
assert_eq!(
functions[0].version(),
"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"
);
assert_eq!(functions[0].version(), "1");
}
#[tokio::test]
@@ -3026,24 +3008,13 @@ mod tests {
assert_eq!(request.url().path(), "/v1/function/embed/drop");
let body: serde_json::Value =
serde_json::from_slice(request.body().unwrap().as_bytes().unwrap()).unwrap();
assert_eq!(
body,
serde_json::json!({"version": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"})
);
assert_eq!(body, serde_json::json!({"version": "1"}));
http::Response::builder()
.status(200)
.body(r#"{"dropped":false}"#)
.unwrap()
});
assert!(
!conn
.drop_function(
"embed",
"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"
)
.await
.unwrap()
);
assert!(!conn.drop_function("embed", "1").await.unwrap());
}
#[tokio::test]
+1 -4
View File
@@ -285,10 +285,7 @@ mod tests {
async fn typed_remote_job_fixtures_decode_terminal_results() {
let function = Job::<FunctionVersion>::new_typed(Box::new(FixtureRemoteJob(FUNCTION_JOB)));
let result = function.wait().await.expect("typed FunctionVersion result");
assert_eq!(
result.version(),
"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"
);
assert_eq!(result.version(), "1");
let refresh =
Job::<RefreshColumnResult>::new_typed(Box::new(FixtureRemoteJob(REFRESH_JOB)));
+4 -4
View File
@@ -7742,7 +7742,7 @@ mod tests {
});
let application = crate::function::FunctionApplication::from_json(
r#"{
"function":{"name":"embed","version":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"function":{"name":"embed","version":"1","object_id":"fixture","location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs":[{"parameter":"text","kind":"column","value":{"path":"description"}}],
"output":{"kind":"scalar","arrow_type":"list<float32>","nullable":false}
}"#,
@@ -7819,7 +7819,7 @@ mod tests {
});
let application = crate::function::FunctionApplication::from_json(
r#"{
"function":{"name":"text_features","version":"fv_01K3TEXT"},
"function":{"name":"text_features","version":"1","object_id":"fixture","location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs":[
{"parameter":"title","kind":"column","value":{"path":"title"}},
{"parameter":"body","kind":"column","value":{"path":"body"}}
@@ -7875,7 +7875,7 @@ mod tests {
});
let application = crate::function::FunctionApplication::from_json(
r#"{
"function":{"name":"embed","version":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"function":{"name":"embed","version":"1","object_id":"fixture","location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs":[{"parameter":"text","kind":"column","value":{"path":"description"}}],
"output":{"kind":"scalar","arrow_type":"fixed_size_list<float32, 3>","nullable":false}
}"#,
@@ -7920,7 +7920,7 @@ mod tests {
});
let application = crate::function::FunctionApplication::from_json(
r#"{
"function":{"name":"text_features","version":"fv_01K3TEXT"},
"function":{"name":"text_features","version":"1","object_id":"fixture","location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs":[
{"parameter":"title","kind":"column","value":{"path":"title"}},
{"parameter":"body","kind":"column","value":{"path":"body"}}
+17 -11
View File
@@ -539,7 +539,13 @@ fn ensure_known_binding_shape(value: &Value) -> Result<()> {
object
.get("function")
.ok_or_else(|| invalid_function("Function binding is missing its exact version"))?,
&["name", "version"],
&[
"name",
"object_id",
"location",
"version",
"manifest_digest",
],
"version reference",
)?;
for input in object
@@ -3022,7 +3028,7 @@ mod tests {
fn named_struct_application(columns: &str) -> FunctionApplication {
FunctionApplication::from_json(&format!(
r#"{{
"function":{{"name":"text_features","version":"fv_exact"}},
"function":{{"name":"text_features","version":"1","object_id":"fixture","location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"}},
"inputs":[
{{"parameter":"title","kind":"column","value":{{"path":"title"}}}},
{{"parameter":"body","kind":"column","value":{{"path":"body"}}}}
@@ -3040,7 +3046,7 @@ mod tests {
fn blob_application(output: &str) -> FunctionApplication {
FunctionApplication::from_json(&format!(
r#"{{
"function":{{"name":"blob_features","version":"fv_blob"}},
"function":{{"name":"blob_features","version":"1","object_id":"fixture","location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"}},
"inputs":[
{{"parameter":"image","kind":"column","value":{{"path":"image"}}}}
],
@@ -3059,7 +3065,7 @@ mod tests {
fn single_input_application(path: &str) -> FunctionApplication {
FunctionApplication::from_json(
&serde_json::json!({
"function": {"name": "inspect", "version": "fv_nested_blob"},
"function": {"name": "inspect", "version": "1","object_id":"fixture","location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs": [{
"parameter": "value",
"kind": "column",
@@ -3401,7 +3407,7 @@ mod tests {
));
let dependent_application = FunctionApplication::from_json(
r#"{
"function":{"name":"dependent","version":"fv_dependent"},
"function":{"name":"dependent","version":"1","object_id":"fixture","location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs":[
{"parameter":"text","kind":"column","value":{"path":"search_text"}}
],
@@ -3532,7 +3538,7 @@ mod tests {
let input = ArrowField::new("value", DataType::Int64, false);
let application = FunctionApplication::from_json(
&serde_json::json!({
"function": {"name": "embed", "version": "fv_embed"},
"function": {"name": "embed", "version": "1","object_id":"fixture","location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs": [{
"parameter": "value",
"kind": "column",
@@ -3737,7 +3743,7 @@ mod tests {
));
let application = FunctionApplication::from_json(
&serde_json::json!({
"function": {"name": "inspect", "version": "fv_nested_blob"},
"function": {"name": "inspect", "version": "1","object_id":"fixture","location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs": [],
"output": {
"kind": "named_struct",
@@ -3869,7 +3875,7 @@ mod tests {
fn test_unknown_and_mixed_version_function_contracts_fail_closed() {
let application = FunctionApplication::from_json(
r#"{
"function":{"name":"f","version":"fv"},
"function":{"name":"f","version":"1","object_id":"fixture","location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs":[{"parameter":"title","kind":"future_source","value":{"path":"title"}}],
"output":{"kind":"scalar","arrow_type":"int64","nullable":false}
}"#,
@@ -3881,7 +3887,7 @@ mod tests {
let future_application = FunctionApplication::from_json(
r#"{
"function":{"name":"f","version":"fv"},
"function":{"name":"f","version":"1","object_id":"fixture","location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs":[],
"output":{"kind":"scalar","arrow_type":"int64","nullable":false},
"future_declaration":{"mode":"managed"}
@@ -3895,7 +3901,7 @@ mod tests {
let nested_future_application = FunctionApplication::from_json(
r#"{
"function":{"name":"f","version":"fv"},
"function":{"name":"f","version":"1","object_id":"fixture","location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs":[],
"output":{"kind":"scalar","arrow_type":"int64","nullable":false,"assignment":"cell_flag"}
}"#,
@@ -3961,7 +3967,7 @@ mod tests {
let nested_schema = ArrowSchema::new(vec![nested_title, schema.field(1).as_ref().clone()]);
let nested_application = FunctionApplication::from_json(
r#"{
"function":{"name":"text_features","version":"fv_exact"},
"function":{"name":"text_features","version":"1","object_id":"fixture","location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs":[
{"parameter":"title","kind":"column","value":{"path":"title.value"}},
{"parameter":"body","kind":"column","value":{"path":"body"}}
@@ -26,11 +26,8 @@ fn function_version_job_result_matches_shared_canonical_golden() {
let version = FunctionVersion::from_json(&result.to_string()).expect("FunctionVersion result");
assert_eq!(version.name(), "embed");
assert_eq!(
version.version(),
"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"
);
assert_eq!(version.image().manifest_digest, version.version());
assert_eq!(version.version(), "1");
assert_ne!(version.image().manifest_digest, version.version());
assert_eq!(
version.to_canonical_json().expect("canonical JSON"),
fixture("remote_function_version.canonical.json").trim()
@@ -48,16 +45,28 @@ fn version_identity_is_immutable_and_exact() {
assert_eq!(reopened.version(), version.version());
let mut changed = original;
changed["version"] = Value::String("fv_01K3DIFFERENT".to_string());
changed["version"] = Value::String("2".to_string());
let changed = FunctionVersion::from_json(&changed.to_string()).expect("changed version");
assert_ne!(changed, version);
assert_eq!(changed.image(), version.image());
for invalid in [
version.image().manifest_digest.as_str(),
"0",
"01",
"-1",
"18446744073709551616",
] {
let mut value = serde_json::to_value(&version).unwrap();
value["version"] = Value::String(invalid.into());
assert!(FunctionVersion::from_json(&value.to_string()).is_err());
}
}
#[test]
fn application_and_binding_match_shared_remote_goldens() {
let application = FunctionApplication::from_json(&fixture("remote_function_application.json"))
.expect("application fixture");
assert_eq!(application.function().version, "fv_01K3TEXT");
assert_eq!(application.function().version, "1");
assert_eq!(application.output().kind, "named_struct");
assert_eq!(application.inputs().len(), 2);
assert_eq!(
@@ -67,7 +76,7 @@ fn application_and_binding_match_shared_remote_goldens() {
let binding = FunctionBinding::from_json(&fixture("remote_function_binding.json"))
.expect("binding fixture");
assert_eq!(binding.function().version, "fv_01K3TEXT");
assert_eq!(binding.function().version, "1");
assert_eq!(binding.outputs()[0].output_ordinal, 0);
assert_eq!(binding.outputs()[1].output_ordinal, 1);
assert!(binding.input_schema().is_some());
@@ -42,11 +42,11 @@ async fn local_function_catalog_operations_return_stable_not_supported() {
let create_error = connection.create_function_async(request).await.unwrap_err();
let lookup_error = connection
.get_function("normalize_score", "fv_exact")
.get_function("normalize_score", "1")
.await
.unwrap_err();
let drop_error = connection
.drop_function("normalize_score", "fv_exact")
.drop_function("normalize_score", "1")
.await
.unwrap_err();
for error in [create_error, lookup_error, drop_error] {
@@ -9,7 +9,7 @@
"application": {
"function": {
"name": "embed",
"version": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"
"version": "1", "object_id": "fixture", "location": "memory:///fixture", "manifest_digest": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"
},
"inputs": [
{
@@ -1 +1 @@
{"columns":{"normalized_text":"search_text","token_count":"search_token_count"},"function":{"name":"text_features","version":"fv_01K3TEXT"},"inputs":[{"kind":"column","parameter":"title","value":{"path":"title"}},{"kind":"column","parameter":"body","value":{"path":"body"}}],"output":{"fields":[{"arrow_type":"utf8","name":"normalized_text","nullable":false},{"arrow_type":"int64","name":"token_count","nullable":false}],"kind":"named_struct"}}
{"columns":{"normalized_text":"search_text","token_count":"search_token_count"},"function":{"location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6","name":"text_features","object_id":"fixture","version":"1"},"inputs":[{"kind":"column","parameter":"title","value":{"path":"title"}},{"kind":"column","parameter":"body","value":{"path":"body"}}],"output":{"fields":[{"arrow_type":"utf8","name":"normalized_text","nullable":false},{"arrow_type":"int64","name":"token_count","nullable":false}],"kind":"named_struct"}}
@@ -1,5 +1,5 @@
{
"function": {"name": "text_features", "version": "fv_01K3TEXT"},
"function": {"name": "text_features", "version": "1", "object_id": "fixture", "location": "memory:///fixture", "manifest_digest": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs": [
{"parameter": "title", "kind": "column", "value": {"path": "title"}},
{"parameter": "body", "kind": "column", "value": {"path": "body"}}
@@ -1,5 +1,5 @@
{
"function": {"name": "score", "version": "fv_01K3FLOAT"},
"function": {"name": "score", "version": "1", "object_id": "fixture", "location": "memory:///fixture", "manifest_digest": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs": [
{"parameter": "threshold", "kind": "literal", "value": 1e-7}
],
@@ -1 +1 @@
{"binding_id":"fb_01K3TEXT","function":{"name":"text_features","version":"fv_01K3TEXT"},"input_schema":{"fields":[{"name":"title","nullable":true,"type":{"type":"utf8"}},{"name":"body","nullable":true,"type":{"type":"utf8"}}]},"inputs":[{"arrow_type":"utf8","field_id":11,"field_path":"title","nullable":true,"parameter":"title"},{"arrow_type":"utf8","field_id":12,"field_path":"body","nullable":true,"parameter":"body"}],"output_schema":{"fields":[{"name":"search_text","nullable":true,"type":{"type":"utf8"}},{"name":"search_token_count","nullable":true,"type":{"type":"int64"}}]},"outputs":[{"arrow_type":"utf8","nullable":false,"output_field_id":21,"output_name":"search_text","output_ordinal":0,"result_field":"normalized_text"},{"arrow_type":"int64","nullable":false,"output_field_id":22,"output_name":"search_token_count","output_ordinal":1,"result_field":"token_count"}]}
{"binding_id":"fb_01K3TEXT","function":{"location":"memory:///fixture","manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6","name":"text_features","object_id":"fixture","version":"1"},"input_schema":{"fields":[{"name":"title","nullable":true,"type":{"type":"utf8"}},{"name":"body","nullable":true,"type":{"type":"utf8"}}]},"inputs":[{"arrow_type":"utf8","field_id":11,"field_path":"title","nullable":true,"parameter":"title"},{"arrow_type":"utf8","field_id":12,"field_path":"body","nullable":true,"parameter":"body"}],"output_schema":{"fields":[{"name":"search_text","nullable":true,"type":{"type":"utf8"}},{"name":"search_token_count","nullable":true,"type":{"type":"int64"}}]},"outputs":[{"arrow_type":"utf8","nullable":false,"output_field_id":21,"output_name":"search_text","output_ordinal":0,"result_field":"normalized_text"},{"arrow_type":"int64","nullable":false,"output_field_id":22,"output_name":"search_token_count","output_ordinal":1,"result_field":"token_count"}]}
@@ -1,6 +1,6 @@
{
"binding_id": "fb_01K3TEXT",
"function": {"name": "text_features", "version": "fv_01K3TEXT"},
"function": {"name": "text_features", "version": "1", "object_id": "fixture", "location": "memory:///fixture", "manifest_digest": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs": [
{"parameter": "title", "field_id": 11, "field_path": "title", "arrow_type": "utf8", "nullable": true},
{"parameter": "body", "field_id": 12, "field_path": "body", "arrow_type": "utf8", "nullable": true}
@@ -8,7 +8,7 @@
},
"result": {
"name": "embed",
"version": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6",
"version": "1", "object_id": "fixture", "location": "memory:///fixture", "metadata": {}, "disabled": false,
"signature": {
"inputs": [
{
@@ -1 +1 @@
{"created_at":"2026-08-21T00:00:00Z","image":{"descriptor":{"behavior":{"result_stability":"input_and_context","side_effects":"non_idempotent"},"entrypoint":"app.function:create","format_version":1,"interface":{"type":"lance.scalar","version":1},"python":{"abi_tag":"cp312","executable":"/usr/local/bin/python3","implementation":"cpython","import_paths":["/opt/function/code"],"version":"3.12.14"},"python_api":1,"requires":{"capabilities":[],"kernel_min":"4.18.0"},"schemas":{"initialization":"/opt/function/schemas/initialization.arrow","input":"/opt/function/schemas/input.arrow","output":"/opt/function/schemas/output.arrow"}},"manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6","source":false},"name":"embed","signature":{"inputs":[{"arrow_type":"utf8","name":"text","nullable":true}],"output":{"arrow_type":"list<float32>","kind":"scalar","nullable":false}},"version":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"}
{"created_at":"2026-08-21T00:00:00Z","disabled":false,"image":{"descriptor":{"behavior":{"result_stability":"input_and_context","side_effects":"non_idempotent"},"entrypoint":"app.function:create","format_version":1,"interface":{"type":"lance.scalar","version":1},"python":{"abi_tag":"cp312","executable":"/usr/local/bin/python3","implementation":"cpython","import_paths":["/opt/function/code"],"version":"3.12.14"},"python_api":1,"requires":{"capabilities":[],"kernel_min":"4.18.0"},"schemas":{"initialization":"/opt/function/schemas/initialization.arrow","input":"/opt/function/schemas/input.arrow","output":"/opt/function/schemas/output.arrow"}},"manifest_digest":"sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6","source":false},"location":"memory:///fixture","metadata":{},"name":"embed","object_id":"fixture","signature":{"inputs":[{"arrow_type":"utf8","name":"text","nullable":true}],"output":{"arrow_type":"list<float32>","kind":"scalar","nullable":false}},"version":"1"}
@@ -5,7 +5,7 @@
],
"function": {
"application": {
"function": {"name": "text_features", "version": "fv_01K3TEXT"},
"function": {"name": "text_features", "version": "1", "object_id": "fixture", "location": "memory:///fixture", "manifest_digest": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs": [
{"parameter": "title", "kind": "column", "value": {"path": "title"}},
{"parameter": "body", "kind": "column", "value": {"path": "body"}}
@@ -4,7 +4,7 @@
],
"function": {
"application": {
"function": {"name": "embed", "version": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"function": {"name": "embed", "version": "1", "object_id": "fixture", "location": "memory:///fixture", "manifest_digest": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6"},
"inputs": [
{"parameter": "text", "kind": "column", "value": {"path": "description"}}
],