From ec44eb72fa207a78d246e94dabf00bab995c62f2 Mon Sep 17 00:00:00 2001 From: Brendan Clement Date: Mon, 5 Oct 2026 15:00:20 -0700 Subject: [PATCH] docs: say which Function outputs may be nullable The page said every Function output must be non-nullable. On this SDK only a scalar output must be. A named-struct output may declare nullable fields: whether a row has a result at all is the struct's own validity, which Enterprise keeps in a separate internal column when it expands the result into sibling columns. test_function_output_nullability checks both rules against the SDK, including that a struct declared as a single nullable field is refused. --- docs/web-tests/py/test_function.py | 31 ++++++++++++++++++++++++++++++ docs/web/management/function.mdx | 9 ++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/web-tests/py/test_function.py b/docs/web-tests/py/test_function.py index a760a54d3..3102678a8 100644 --- a/docs/web-tests/py/test_function.py +++ b/docs/web-tests/py/test_function.py @@ -21,6 +21,7 @@ import sys from pathlib import Path import lancedb +import pyarrow as pa import pytest from lancedb import col from lancedb.functions import FunctionApplication, FunctionVersion @@ -36,6 +37,11 @@ def double(value: float) -> float: # --8<-- [end:function_define] + +def describe(value: float): + return {"half": value / 2, "note": None} + + # A Function version as the service returns it after registration: the SDK's # shared golden reply, with the name and signature of the definition above. SERVICE_REPLY = ( @@ -92,6 +98,31 @@ def test_function_registration_request(): assert request.runtime.environment.packages == () +def test_function_output_nullability(): + inputs = pa.schema([pa.field("value", pa.float64(), nullable=False)]) + + # A named-struct output may declare nullable fields. + fields = [ + pa.field("half", pa.float64(), nullable=False), + pa.field("note", pa.string(), nullable=True), + ] + struct = udf(input_schema=inputs, output_schema=pa.schema(fields))(describe) + output = struct.registration_request.signature.output + assert output.kind == "named_struct" + assert [(field.name, field.nullable) for field in output.fields] == [ + ("half", False), + ("note", True), + ] + + # A scalar output, and a struct declared as one nullable field, may not be. + for nullable in ( + pa.field("result", pa.float64(), nullable=True), + pa.field("result", pa.struct(fields), nullable=True), + ): + with pytest.raises(ValueError, match="non-nullable"): + udf(input_schema=inputs, output_schema=nullable)(describe) + + def test_function_registration_is_remote(tmp_path): db = lancedb.connect(tmp_path) with pytest.raises(NotImplementedError): diff --git a/docs/web/management/function.mdx b/docs/web/management/function.mdx index b1df47901..ce79ad605 100644 --- a/docs/web/management/function.mdx +++ b/docs/web/management/function.mdx @@ -53,9 +53,12 @@ your process, without the database, so unit tests stay ordinary tests: Where annotations cannot express the Arrow type precisely, pass `input_schema` -and `output_schema` together — one without the other is rejected. Outputs must -be non-nullable, because a physical NULL is what marks a computed-column row as -unfilled. +and `output_schema` together — one without the other is rejected. A scalar +output must be non-nullable, because a physical NULL is what marks a +computed-column row as unfilled. A named-struct output may declare nullable +fields: whether a row has a result at all is the struct's own validity, which +Enterprise keeps in a separate internal column when it expands the result into +sibling columns, so a null field is a value rather than a missing row. ## Connect and create a table {#table}