mirror of
https://github.com/lancedb/lancedb.git
synced 2026-09-30 00:45:37 +00:00
feat: carry Function initialization in the canonical model (#4253)
A Function instance is created once with an initialization row (`create(initialization, context)` in the Function Format), but nothing in the client model could say what that row holds or where its values come from, so every source-built Function ran with an empty row and users pushed configuration through `env=` strings instead (ENT-2516). This adds initialization to the canonical wire model without changing a Function's identity: - `FunctionSignature.initialization` lists the fields of the row, in order. It is omitted when empty, so existing signatures and FunctionVersion hashes are unchanged. - `FunctionApplication.initialization` carries a binding's values as JSON. The service validates them against the Function's fields and re-encodes them as Arrow, so this JSON is transport only and never hashed; that is why floats are accepted here while column-input literals keep the Slice 1 domain. - `FunctionBinding.initialization` is the validated one-row Arrow IPC stream (base64) that every instance of the binding is created with. Values belong to the binding, so one Function version can back several columns with different values. - The binding-shape check that guards schema mutations accepts the new field; without it a table holding an initialized binding refused further declarations. Rust and Python share new canonical goldens for an initialized application and binding. The authoring side (class `@udf`, initialization arguments, shipped helper code) is the stacked PR on top of this one.
This commit is contained in:
@@ -256,8 +256,16 @@ class SecretBinding(_RemoteValue):
|
||||
|
||||
|
||||
class FunctionSignature(_RemoteValue):
|
||||
"""Ordered inputs, output, and initialization fields of a Function.
|
||||
|
||||
``initialization`` lists the fields of the single row each Function
|
||||
instance is created with. Values come from each binding, not from the
|
||||
Function version; the tuple is empty when the Function takes none.
|
||||
"""
|
||||
|
||||
inputs: tuple[FunctionParameter, ...]
|
||||
output: FunctionOutput
|
||||
initialization: tuple[FunctionParameter, ...] = ()
|
||||
|
||||
|
||||
class PythonEnvironmentSpec(_RemoteValue):
|
||||
@@ -478,6 +486,10 @@ class FunctionApplication(_OpenRemoteValue):
|
||||
inputs: tuple[ApplicationInput, ...]
|
||||
output: FunctionOutput
|
||||
columns: Mapping[str, str] = Field(default_factory=dict)
|
||||
# Transport JSON: the service validates it against the Function's
|
||||
# initialization schema and persists the Arrow row in the binding, so it is
|
||||
# never hashed and floating-point values are allowed.
|
||||
initialization: Mapping[str, Any] = Field(default_factory=dict)
|
||||
|
||||
def _known_dict(self) -> dict[str, Any]:
|
||||
value = super()._known_dict()
|
||||
@@ -560,6 +572,15 @@ class FunctionBinding(_RemoteValue):
|
||||
assignment: Optional[AssignmentMapping] = None
|
||||
input_schema: Optional[Mapping[str, Any]] = None
|
||||
output_schema: Optional[Mapping[str, Any]] = None
|
||||
initialization: Optional[str] = None
|
||||
"""Standard base64 of the validated one-row Arrow IPC initialization stream."""
|
||||
|
||||
def initialization_row(self) -> Optional[dict[str, Any]]:
|
||||
"""The initialization values this binding creates instances with."""
|
||||
if self.initialization is None:
|
||||
return None
|
||||
reader = pa.ipc.open_stream(base64.b64decode(self.initialization))
|
||||
return reader.read_all().to_pylist()[0]
|
||||
|
||||
|
||||
class RefreshColumnResult(_RemoteValue):
|
||||
|
||||
@@ -83,6 +83,18 @@ def test_public_function_values_are_in_api_reference():
|
||||
FunctionBinding,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"remote_initialized_function_application.json",
|
||||
"remote_initialized_function_application.canonical.json",
|
||||
FunctionApplication,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"remote_initialized_function_binding.json",
|
||||
"remote_initialized_function_binding.canonical.json",
|
||||
FunctionBinding,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"remote_refresh_job.json",
|
||||
"remote_refresh_result.canonical.json",
|
||||
@@ -269,6 +281,35 @@ def test_binding_and_refresh_result_keep_stable_remote_fields():
|
||||
assert RefreshColumnResult.from_json(result.to_canonical_json()) == result
|
||||
|
||||
|
||||
def test_initialization_travels_on_the_application_and_binding():
|
||||
application = FunctionApplication.from_json(
|
||||
fixture("remote_initialized_function_application.json")
|
||||
)
|
||||
assert application.initialization["temperature"] == 0.25
|
||||
assert application.initialization["retry"]["backoff"] == (1, 2, 4)
|
||||
application._ensure_declarable()
|
||||
|
||||
binding = FunctionBinding.from_json(
|
||||
fixture("remote_initialized_function_binding.json")
|
||||
)
|
||||
assert binding.initialization_row() == {
|
||||
"model": "text-embedding-3-small",
|
||||
"dimensions": 512,
|
||||
}
|
||||
assert (
|
||||
FunctionBinding.from_json(
|
||||
fixture("remote_function_binding.json")
|
||||
).initialization_row()
|
||||
is None
|
||||
)
|
||||
|
||||
signature = FunctionVersion.from_json(
|
||||
json.dumps(job_result("remote_function_job.json"))
|
||||
).signature
|
||||
assert signature.initialization == ()
|
||||
assert "initialization" not in json.loads(signature.to_canonical_json())
|
||||
|
||||
|
||||
def test_function_literal_numeric_domain_matches_rust():
|
||||
with pytest.raises(ValueError, match="floating-point Function literals"):
|
||||
FunctionApplication.from_json(fixture("remote_function_application_float.json"))
|
||||
|
||||
Reference in New Issue
Block a user