mirror of
https://github.com/lancedb/lancedb.git
synced 2026-09-21 20:45:57 +00:00
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>
153 lines
5.6 KiB
Rust
153 lines
5.6 KiB
Rust
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
|
|
use lancedb::function::{
|
|
FunctionApplication, FunctionBinding, FunctionVersion, RefreshColumnResult,
|
|
};
|
|
use serde_json::Value;
|
|
|
|
fn fixture(name: &str) -> String {
|
|
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests/fixtures/first_class_functions/v1")
|
|
.join(name);
|
|
fs::read_to_string(path).expect("fixture must be readable")
|
|
}
|
|
|
|
fn job_result(name: &str) -> Value {
|
|
serde_json::from_str::<Value>(&fixture(name)).expect("remote Job fixture")["result"].clone()
|
|
}
|
|
|
|
#[test]
|
|
fn function_version_job_result_matches_shared_canonical_golden() {
|
|
let result = job_result("remote_function_job.json");
|
|
let version = FunctionVersion::from_json(&result.to_string()).expect("FunctionVersion result");
|
|
|
|
assert_eq!(version.name(), "embed");
|
|
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()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn version_identity_is_immutable_and_exact() {
|
|
let original = job_result("remote_function_job.json");
|
|
let version =
|
|
FunctionVersion::from_json(&original.to_string()).expect("FunctionVersion result");
|
|
let reopened = version.clone();
|
|
assert_eq!(reopened, version);
|
|
assert_eq!(reopened.name(), version.name());
|
|
assert_eq!(reopened.version(), version.version());
|
|
|
|
let mut changed = original;
|
|
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, "1");
|
|
assert_eq!(application.output().kind, "named_struct");
|
|
assert_eq!(application.inputs().len(), 2);
|
|
assert_eq!(
|
|
application.to_canonical_json().expect("canonical JSON"),
|
|
fixture("remote_function_application.canonical.json").trim()
|
|
);
|
|
|
|
let binding = FunctionBinding::from_json(&fixture("remote_function_binding.json"))
|
|
.expect("binding fixture");
|
|
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());
|
|
assert!(binding.output_schema().is_some());
|
|
assert_eq!(
|
|
binding.to_canonical_json().expect("canonical JSON"),
|
|
fixture("remote_function_binding.canonical.json").trim()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn refresh_job_result_matches_shared_canonical_golden() {
|
|
let result = job_result("remote_refresh_job.json");
|
|
let result = RefreshColumnResult::from_json(&result.to_string()).expect("refresh result");
|
|
assert_eq!(result.rows_assigned, 999_998_800);
|
|
assert_eq!(result.rows_filled(), result.rows_assigned);
|
|
assert_eq!(result.version(), result.published_version);
|
|
assert_eq!(
|
|
result.to_canonical_json().expect("canonical JSON"),
|
|
fixture("remote_refresh_result.canonical.json").trim()
|
|
);
|
|
|
|
let result = RefreshColumnResult::from_json(&fixture(
|
|
"remote_refresh_result_without_published_version.json",
|
|
))
|
|
.expect("optional version");
|
|
assert_eq!(result.published_version, None);
|
|
assert_eq!(
|
|
result
|
|
.to_canonical_json()
|
|
.expect("canonical result without version"),
|
|
fixture("remote_refresh_result_without_published_version.canonical.json").trim()
|
|
);
|
|
assert_eq!(
|
|
RefreshColumnResult::from_json(
|
|
&result
|
|
.to_canonical_json()
|
|
.expect("canonical result without version")
|
|
)
|
|
.expect("round-trip result without version"),
|
|
result
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_fields_and_discriminators_are_forward_decodable() {
|
|
let mut result = job_result("remote_function_job.json");
|
|
result["future_version_metadata"] = serde_json::json!({"retention_class": "catalog"});
|
|
result["image"]["descriptor"]["future_interface"] = serde_json::json!({"version": 2});
|
|
result["signature"]["output"]["kind"] = Value::String("future_output_shape".to_string());
|
|
|
|
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::<Value>(
|
|
&version.to_canonical_json().expect("canonical future value")
|
|
)
|
|
.expect("canonical JSON")["image"]["descriptor"]["future_interface"],
|
|
serde_json::json!({"version": 2})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn floating_point_application_literals_are_rejected_consistently() {
|
|
let error = FunctionApplication::from_json(&fixture("remote_function_application_float.json"))
|
|
.unwrap_err();
|
|
assert!(
|
|
error
|
|
.to_string()
|
|
.contains("floating-point Function literals")
|
|
);
|
|
}
|