Skip to content

Commit 39ad674

Browse files
rustyconoverclaude
andcommitted
fixtures: blended_any / blended_any_varargs — ANY-typed blended input columns
Two RowTransformFunction fixtures whose positional input column(s) are declared AnyArrow. Both are 1->1 echoes: on_bind types the output from the input schema the client supplies (value, or col0..colN-1 for the varargs form) and process() passes the columns through untouched. An ANY arg names no concrete Arrow type, so this only works when the client builds the worker-input schema from the type DuckDB resolved for the call — Query-farm/vgi d39ca9e. Backs the cross-SDK conformance file table_in_out/blended_any.test and the 164 -> 166 portable inventory. Tests: exchange-level coverage over string / int32 / struct / list inputs (a null struct and a struct with a null field stay distinct) and per-column typing for varargs; the ANY marker on the argument spec and its survival through the wire arguments schema. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent cd3a2fc commit 39ad674

4 files changed

Lines changed: 198 additions & 1 deletion

File tree

‎tests/table_in_out/test_blended_exchange.py‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,73 @@ def test_output_identical_with_and_without_has_finalize(self, client_transport:
280280
assert len(with_finalize) == 1
281281
assert len(without_finalize) == 1
282282
assert with_finalize[0].column("i").to_pylist() == without_finalize[0].column("i").to_pylist()
283+
284+
285+
class TestBlendedAny:
286+
"""`blended_any` / `blended_any_varargs`: positional input columns declared ANY.
287+
288+
An ANY arg names no concrete Arrow type, so the output type can only come from
289+
the input schema the client supplies at bind. These drive that over the wire:
290+
the same registration must bind to whatever type each call resolves to, and
291+
echo the column back untouched (nested types and nulls included).
292+
"""
293+
294+
@pytest.mark.parametrize(
295+
"array",
296+
[
297+
pa.array(["a", None, "c"], type=pa.string()),
298+
pa.array([1, 2, None], type=pa.int32()),
299+
pa.array(
300+
[{"message": "lost pkg", "tier": "gold"}, None, {"message": None, "tier": "free"}],
301+
type=pa.struct([("message", pa.string()), ("tier", pa.string())]),
302+
),
303+
pa.array([[1, 2], [], None], type=pa.list_(pa.int64())),
304+
],
305+
ids=["string", "int32", "struct", "list"],
306+
)
307+
def test_output_is_typed_from_the_input_and_echoed(self, fixture_worker: str, array: pa.Array[Any]) -> None:
308+
"""One registration, bound per call to the resolved input type."""
309+
batch = pa.record_batch({"value": array})
310+
bound: list[pa.Schema] = []
311+
312+
with Client(fixture_worker) as client:
313+
results = list(
314+
client.table_in_out_function(
315+
function_name="blended_any",
316+
schema_path=["main"],
317+
input=iter([batch]),
318+
bind_result_callback=lambda r: bound.append(r.output_schema),
319+
has_finalize=False,
320+
)
321+
)
322+
323+
assert [s.names for s in bound] == [["value"]]
324+
assert bound[0].field("value").type == array.type
325+
(out,) = results
326+
assert out.column("value").type == array.type
327+
# A null struct and a struct with a null field must stay distinct.
328+
assert out.column("value").to_pylist() == array.to_pylist()
329+
330+
def test_varargs_columns_each_keep_their_own_type(self, fixture_worker: str) -> None:
331+
"""The vararg element type is ANY, so every runtime column types itself."""
332+
batch = pa.record_batch(
333+
{
334+
"col0": pa.array(["a", "b"], type=pa.string()),
335+
"col1": pa.array([[1], [2, 3]], type=pa.list_(pa.int32())),
336+
"col2": pa.array([{"k": 1.5}, None], type=pa.struct([("k", pa.float64())])),
337+
}
338+
)
339+
340+
with Client(fixture_worker) as client:
341+
(out,) = list(
342+
client.table_in_out_function(
343+
function_name="blended_any_varargs",
344+
schema_path=["main"],
345+
input=iter([batch]),
346+
has_finalize=False,
347+
)
348+
)
349+
350+
assert out.schema.names == ["col0", "col1", "col2"]
351+
assert out.schema.types == batch.schema.types
352+
assert out.to_pylist() == batch.to_pylist()

‎tests/table_in_out/test_blended_metadata.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,43 @@ class Meta:
148148

149149
with pytest.raises(TypeError, match="at least one positional Arg"):
150150
resolve_metadata(_NamedOnly)
151+
152+
153+
class TestBlendedAnyResolution:
154+
"""A blended function may declare its positional input column(s) ANY.
155+
156+
The ANY marker lives on the argument *spec* — the ``vgi_type=any`` field
157+
metadata the client reads to learn it must take the column's type from the
158+
call rather than from the declaration.
159+
"""
160+
161+
def test_any_positional_is_a_blended_input_column(self) -> None:
162+
"""The ANY arg resolves as a positional input column, not a bind constant."""
163+
from vgi._test_fixtures.table_in_out import BlendedAnyFunction
164+
from vgi.argument_spec import extract_argument_specs
165+
166+
m = resolve_metadata(BlendedAnyFunction)
167+
assert m.input_from_args is True
168+
assert m.has_finalize is False
169+
(spec,) = extract_argument_specs(BlendedAnyFunction)
170+
assert (spec.name, spec.position, spec.is_any_type, spec.is_varargs) == ("value", 0, True, False)
171+
172+
def test_any_varargs_is_a_blended_input_column(self) -> None:
173+
"""The varargs form keeps both markers: ANY element type and varargs."""
174+
from vgi._test_fixtures.table_in_out import BlendedAnyVarargsFunction
175+
from vgi.argument_spec import extract_argument_specs
176+
177+
assert resolve_metadata(BlendedAnyVarargsFunction).input_from_args is True
178+
(spec,) = extract_argument_specs(BlendedAnyVarargsFunction)
179+
assert (spec.name, spec.position, spec.is_any_type, spec.is_varargs) == ("values", 0, True, True)
180+
181+
def test_any_marker_survives_the_wire_schema(self) -> None:
182+
"""The client learns the arg is ANY from the serialized arguments schema."""
183+
from vgi._test_fixtures.table_in_out import BlendedAnyFunction, BlendedAnyVarargsFunction
184+
from vgi.argument_spec import argument_specs_to_schema, extract_argument_specs, schema_to_argument_specs
185+
186+
for function in (BlendedAnyFunction, BlendedAnyVarargsFunction):
187+
specs = extract_argument_specs(function)
188+
wire = schema_to_argument_specs(argument_specs_to_schema(specs))
189+
assert [s.is_any_type for s in wire] == [True]
190+
assert [s.is_varargs for s in wire] == [s.is_varargs for s in specs]

‎vgi/_test_fixtures/table_in_out.py‎

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
from vgi_rpc.log import Level
5252
from vgi_rpc.rpc import OutputCollector
5353

54-
from vgi.arguments import Arg, Setting, TableInput
54+
from vgi.arguments import AnyArrow, Arg, Setting, TableInput
5555
from vgi.cache_control import CacheControl
5656
from vgi.invocation import BindResponse
5757
from vgi.metadata import FunctionExample
@@ -94,6 +94,8 @@ class _LogDrainState(ArrowSerializableDataclass):
9494
"GeoEncode3Function",
9595
"RowSumFunction",
9696
"BlendedDropFunction",
97+
"BlendedAnyFunction",
98+
"BlendedAnyVarargsFunction",
9799
"SubstreamPartialSumFunction",
98100
"MultiBatchFinishFunction",
99101
"SumAllColumnsFunction",
@@ -1880,6 +1882,87 @@ def process(
18801882
cast("VgiOutputCollector", out).emit(out_batch, parent_rows=parent_rows)
18811883

18821884

1885+
@dataclass(slots=True, frozen=True, kw_only=True)
1886+
class _AnyArgs:
1887+
"""One positional ANY-typed input column."""
1888+
1889+
value: Annotated[AnyArrow, Arg(0, doc="Input column of any type (echoed back unchanged)")]
1890+
1891+
1892+
class BlendedAnyFunction(RowTransformFunction[_AnyArgs]):
1893+
"""Blended 1->1 echo of a single ANY-typed input column.
1894+
1895+
Proves a blended positional arg may be declared ANY: the declared type names
1896+
no concrete Arrow type, so the client must build the worker's input schema
1897+
from the type DuckDB actually resolved for the call (a STRUCT built per row,
1898+
a whole-row struct, a LIST, a plain VARCHAR ...) rather than from the
1899+
declaration. The output column ``value`` is bound to that resolved input
1900+
type at bind time and the column is passed through untouched, so a test
1901+
asserts on DuckDB's own ``typeof(value)`` and on the round-tripped values —
1902+
nothing here depends on how an SDK happens to render a type name.
1903+
"""
1904+
1905+
class Meta:
1906+
name = "blended_any"
1907+
description = "Blended 1->1 echo of one ANY-typed input column (output typed from the input)"
1908+
categories = ["blended", "test"]
1909+
1910+
@classmethod
1911+
def on_bind(cls, params: BindParams[_AnyArgs]) -> BindResponse:
1912+
input_schema = params.bind_call.input_schema
1913+
assert input_schema is not None
1914+
return BindResponse(output_schema=pa.schema([pa.field("value", input_schema.field(0).type)]))
1915+
1916+
@classmethod
1917+
def process(
1918+
cls,
1919+
params: ProcessParams[_AnyArgs],
1920+
state: None,
1921+
batch: pa.RecordBatch,
1922+
out: OutputCollector,
1923+
) -> None:
1924+
out.emit(pa.record_batch([batch.column(0)], schema=params.output_schema))
1925+
1926+
1927+
@dataclass(slots=True, frozen=True, kw_only=True)
1928+
class _AnyVarargsArgs:
1929+
"""N positional ANY-typed input columns, each free to be a different type."""
1930+
1931+
values: Annotated[list[AnyArrow], Arg(0, varargs=True, doc="Input columns of any types (echoed back)")]
1932+
1933+
1934+
class BlendedAnyVarargsFunction(RowTransformFunction[_AnyVarargsArgs]):
1935+
"""Blended 1->1 echo of N ANY-typed VARARGS input columns.
1936+
1937+
The varargs counterpart of ``blended_any``: every runtime column may resolve
1938+
to a different concrete type, so the client must take each column's type
1939+
from the call rather than from the (ANY) vararg element type. Output columns
1940+
are ``col0..colN-1`` — the names the client generates for varargs blended
1941+
input — each bound to its own resolved input type.
1942+
"""
1943+
1944+
class Meta:
1945+
name = "blended_any_varargs"
1946+
description = "Blended 1->1 echo of N ANY-typed varargs input columns (col0..colN-1)"
1947+
categories = ["blended", "test"]
1948+
1949+
@classmethod
1950+
def on_bind(cls, params: BindParams[_AnyVarargsArgs]) -> BindResponse:
1951+
input_schema = params.bind_call.input_schema
1952+
assert input_schema is not None
1953+
return BindResponse(output_schema=pa.schema([pa.field(f"col{i}", f.type) for i, f in enumerate(input_schema)]))
1954+
1955+
@classmethod
1956+
def process(
1957+
cls,
1958+
params: ProcessParams[_AnyVarargsArgs],
1959+
state: None,
1960+
batch: pa.RecordBatch,
1961+
out: OutputCollector,
1962+
) -> None:
1963+
out.emit(pa.record_batch(cls.input_columns(batch), schema=params.output_schema))
1964+
1965+
18831966
class CachedExplodeFunction(RowTransformFunction[_ExplodeArgs]):
18841967
"""Cacheable blended 1->N fan-out advertising vgi.cache.per_value.
18851968

‎vgi/_test_fixtures/worker.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@
267267
)
268268
from vgi._test_fixtures.table_in_out import (
269269
BatchIndexBufferInputFunction,
270+
BlendedAnyFunction,
271+
BlendedAnyVarargsFunction,
270272
BlendedDropFunction,
271273
BlendedExplodeFunction,
272274
BufferEmitWideFunction,
@@ -432,6 +434,8 @@ def _build_fixture_stats() -> tuple[
432434
GeoEncode3Function,
433435
RowSumFunction,
434436
BlendedDropFunction,
437+
BlendedAnyFunction,
438+
BlendedAnyVarargsFunction,
435439
BlendedExplodeFunction,
436440
CachedExplodeFunction,
437441
ProjectableBlendedFunction,

0 commit comments

Comments
 (0)