From e6b04c5da7822c48bddb7f1a7982ffdaf00a53df Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Thu, 5 Nov 2026 15:37:16 +0800 Subject: [PATCH] docs(function): define forward-compatible version projection (#4863) Clarify `FunctionVersion` forward compatibility: unknown top-level fields are accepted during decoding and omitted from canonical JSON, while all defined fields and the open `image.descriptor` content retain their values. Extend the existing Rust and Python canonical round-trip tests to assert the complete projected object, including future discriminator values. This documents and tests the existing behavior; it does not change runtime serialization or promise lossless retention of unknown top-level fields. Related: ENT-2176. --- python/python/lancedb/functions.py | 6 ++++++ .../tests/test_first_class_function_slice1.py | 11 ++++++++--- rust/lancedb/src/function.rs | 6 ++++++ .../tests/first_class_function_slice1.rs | 18 +++++++++++------- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/python/python/lancedb/functions.py b/python/python/lancedb/functions.py index 0257e4d7b..fb606cd36 100644 --- a/python/python/lancedb/functions.py +++ b/python/python/lancedb/functions.py @@ -469,6 +469,12 @@ _ObjectVersion = Annotated[ class FunctionVersion(_RemoteValue): """A pinned object revision, independent of its executable image digest. + Unknown top-level fields from newer servers are accepted for forward + decoding, but they are not exposed on this value and are omitted from + ``to_canonical_json()``. Known fields retain their complete values, while + unknown fields inside the open ``image.descriptor`` mapping remain + available. + ``name`` and ``namespace_path`` are the Function's parts, as a [SecretReference][lancedb.functions.SecretReference] carries a Secret's; they are what [get_function][lancedb.db.DBConnection.get_function] takes. diff --git a/python/python/tests/test_first_class_function_slice1.py b/python/python/tests/test_first_class_function_slice1.py index 980c11993..27a9a9376 100644 --- a/python/python/tests/test_first_class_function_slice1.py +++ b/python/python/tests/test_first_class_function_slice1.py @@ -301,10 +301,15 @@ def test_unknown_fields_and_discriminators_are_forward_decodable(): version = FunctionVersion.from_json(json.dumps(value)) 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" + assert not hasattr(version, "future_version_metadata") + + canonical = json.loads(version.to_canonical_json()) + expected = json.loads(fixture("remote_function_version.canonical.json")) + expected["image"]["descriptor"]["future_interface"] = {"kind": "wasm"} + expected["signature"]["output"]["kind"] = "future_output_shape" + assert canonical == expected + assert "future_version_metadata" not in canonical def test_function_application_uses_rename_columns_only(): diff --git a/rust/lancedb/src/function.rs b/rust/lancedb/src/function.rs index 5f322a8f4..c68f29923 100644 --- a/rust/lancedb/src/function.rs +++ b/rust/lancedb/src/function.rs @@ -570,6 +570,12 @@ impl<'de> Deserialize<'de> for RowTimeoutSeconds { /// /// The Function is named by its parts, `name` and `namespace_path`, the way a /// [`SecretReference`](crate::secrets::SecretReference) names a Secret. +/// +/// Unknown top-level fields from newer servers are accepted for forward +/// decoding, but they are not part of this value and are omitted from +/// canonical JSON. Known fields retain their complete values, while unknown +/// fields inside the open [`FunctionImage::descriptor`] JSON object remain +/// available. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct FunctionVersion { name: String, diff --git a/rust/lancedb/tests/first_class_function_slice1.rs b/rust/lancedb/tests/first_class_function_slice1.rs index 86ccea1cc..878e74bb0 100644 --- a/rust/lancedb/tests/first_class_function_slice1.rs +++ b/rust/lancedb/tests/first_class_function_slice1.rs @@ -182,13 +182,17 @@ fn unknown_fields_and_discriminators_are_forward_decodable() { let version = FunctionVersion::from_json(&result.to_string()).expect("future remote value"); assert_eq!(version.image().descriptor["future_interface"]["version"], 2); assert_eq!(version.signature().output.kind, "future_output_shape"); - assert_eq!( - serde_json::from_str::( - &version.to_canonical_json().expect("canonical future value") - ) - .expect("canonical JSON")["image"]["descriptor"]["future_interface"], - serde_json::json!({"version": 2}) - ); + let canonical: Value = + serde_json::from_str(&version.to_canonical_json().expect("canonical future value")) + .expect("canonical JSON"); + + let mut expected: Value = + serde_json::from_str(fixture("remote_function_version.canonical.json").trim()) + .expect("canonical FunctionVersion fixture"); + expected["image"]["descriptor"]["future_interface"] = serde_json::json!({"version": 2}); + expected["signature"]["output"]["kind"] = Value::String("future_output_shape".to_string()); + assert_eq!(canonical, expected); + assert!(canonical.get("future_version_metadata").is_none()); } #[test]