diff --git a/python/python/lancedb/db.py b/python/python/lancedb/db.py index 7ac96408e..d99806b76 100644 --- a/python/python/lancedb/db.py +++ b/python/python/lancedb/db.py @@ -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]: diff --git a/python/python/lancedb/functions.py b/python/python/lancedb/functions.py index ca5b396f2..692bc5756 100644 --- a/python/python/lancedb/functions.py +++ b/python/python/lancedb/functions.py @@ -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): diff --git a/python/python/tests/test_first_class_function_slice1.py b/python/python/tests/test_first_class_function_slice1.py index 124f9a6ef..459212b0f 100644 --- a/python/python/tests/test_first_class_function_slice1.py +++ b/python/python/tests/test_first_class_function_slice1.py @@ -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", diff --git a/python/python/tests/test_first_class_function_slice2.py b/python/python/tests/test_first_class_function_slice2.py index 16038b67a..8d4f75f67 100644 --- a/python/python/tests/test_first_class_function_slice2.py +++ b/python/python/tests/test_first_class_function_slice2.py @@ -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}, ) ] diff --git a/rust/lancedb/src/connection.rs b/rust/lancedb/src/connection.rs index 8b0c16537..00dd1836d 100644 --- a/rust/lancedb/src/connection.rs +++ b/rust/lancedb/src/connection.rs @@ -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 diff --git a/rust/lancedb/src/database.rs b/rust/lancedb/src/database.rs index 41e5768e0..7ee000453 100644 --- a/rust/lancedb/src/database.rs +++ b/rust/lancedb/src/database.rs @@ -313,7 +313,7 @@ pub trait Database: async fn list_functions(&self) -> Result> { 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 { function_catalog_not_supported() } diff --git a/rust/lancedb/src/function.rs b/rust/lancedb/src/function.rs index 7d679027b..d501541c0 100644 --- a/rust/lancedb/src/function.rs +++ b/rust/lancedb/src/function.rs @@ -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 { + let value = String::deserialize(deserializer)?; + match value.parse::() { + 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, + 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 { + &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. diff --git a/rust/lancedb/src/remote/db.rs b/rust/lancedb/src/remote/db.rs index de291ad0e..d42472c29 100644 --- a/rust/lancedb/src/remote/db.rs +++ b/rust/lancedb/src/remote/db.rs @@ -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] diff --git a/rust/lancedb/src/remote/job.rs b/rust/lancedb/src/remote/job.rs index 519e563d4..80256421a 100644 --- a/rust/lancedb/src/remote/job.rs +++ b/rust/lancedb/src/remote/job.rs @@ -285,10 +285,7 @@ mod tests { async fn typed_remote_job_fixtures_decode_terminal_results() { let function = Job::::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::::new_typed(Box::new(FixtureRemoteJob(REFRESH_JOB))); diff --git a/rust/lancedb/src/remote/table.rs b/rust/lancedb/src/remote/table.rs index 7eae6edf7..c8695b3c8 100644 --- a/rust/lancedb/src/remote/table.rs +++ b/rust/lancedb/src/remote/table.rs @@ -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","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","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"}} diff --git a/rust/lancedb/src/table/computed_columns.rs b/rust/lancedb/src/table/computed_columns.rs index a70ac31ae..f87d42426 100644 --- a/rust/lancedb/src/table/computed_columns.rs +++ b/rust/lancedb/src/table/computed_columns.rs @@ -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"}} diff --git a/rust/lancedb/tests/first_class_function_slice1.rs b/rust/lancedb/tests/first_class_function_slice1.rs index c39a0920f..b70ac7f0a 100644 --- a/rust/lancedb/tests/first_class_function_slice1.rs +++ b/rust/lancedb/tests/first_class_function_slice1.rs @@ -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()); diff --git a/rust/lancedb/tests/first_class_function_slice2.rs b/rust/lancedb/tests/first_class_function_slice2.rs index 6d046d9d1..65924f394 100644 --- a/rust/lancedb/tests/first_class_function_slice2.rs +++ b/rust/lancedb/tests/first_class_function_slice2.rs @@ -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] { diff --git a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_fixed_size_declaration_request.json b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_fixed_size_declaration_request.json index 19a5452a6..1c2374fba 100644 --- a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_fixed_size_declaration_request.json +++ b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_fixed_size_declaration_request.json @@ -9,7 +9,7 @@ "application": { "function": { "name": "embed", - "version": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6" + "version": "1", "object_id": "fixture", "location": "memory:///fixture", "manifest_digest": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6" }, "inputs": [ { diff --git a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_application.canonical.json b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_application.canonical.json index b91dd1061..8efa681aa 100644 --- a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_application.canonical.json +++ b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_application.canonical.json @@ -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"}} diff --git a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_application.json b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_application.json index 177821aaa..d5b4fd5d8 100644 --- a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_application.json +++ b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_application.json @@ -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"}} diff --git a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_application_float.json b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_application_float.json index c23c8f3ca..98dafcfab 100644 --- a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_application_float.json +++ b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_application_float.json @@ -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} ], diff --git a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_binding.canonical.json b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_binding.canonical.json index 143190f78..f1355364a 100644 --- a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_binding.canonical.json +++ b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_binding.canonical.json @@ -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"}]} diff --git a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_binding.json b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_binding.json index dec3fa3f8..ae1666544 100644 --- a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_binding.json +++ b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_binding.json @@ -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} diff --git a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_job.json b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_job.json index 49e7ef61b..3381fd266 100644 --- a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_job.json +++ b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_job.json @@ -8,7 +8,7 @@ }, "result": { "name": "embed", - "version": "sha256:7e22f815b6648e14f093a3979a8e5a2082fa773ebe1ec84b135cae7e84d6f8e6", + "version": "1", "object_id": "fixture", "location": "memory:///fixture", "metadata": {}, "disabled": false, "signature": { "inputs": [ { diff --git a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_version.canonical.json b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_version.canonical.json index a7a2be434..3f0c11973 100644 --- a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_version.canonical.json +++ b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_function_version.canonical.json @@ -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","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","kind":"scalar","nullable":false}},"version":"1"} diff --git a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_multi_output_declaration_request.json b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_multi_output_declaration_request.json index c4ef5009f..02b2d5880 100644 --- a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_multi_output_declaration_request.json +++ b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_multi_output_declaration_request.json @@ -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"}} diff --git a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_scalar_declaration_request.json b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_scalar_declaration_request.json index a983a3c3d..30fe06b91 100644 --- a/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_scalar_declaration_request.json +++ b/rust/lancedb/tests/fixtures/first_class_functions/v1/remote_scalar_declaration_request.json @@ -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"}} ],