Skip to content

Commit ef1c1d8

Browse files
rustyconoverclaude
andcommitted
Replace catalog_output_type() with Meta.output_type for scalar functions
This is a breaking change that simplifies scalar function definitions: - Add ScalarOutputType type alias for pa.DataType | type[AnyArrow] - Add output_type to valid Meta attributes in metadata.py - Add _get_meta_output_type() helper to read output_type from Meta class - Remove abstract catalog_output_type() classmethod - Update catalog_output_schema() and output_type property to use Meta - Update all example functions and tests to use Meta.output_type - Update CLAUDE.md documentation with new pattern Before: @classmethod def catalog_output_type(cls) -> pa.DataType | type[AnyArrow]: return pa.string() After: class Meta: output_type = pa.string() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2f13761 commit ef1c1d8

6 files changed

Lines changed: 113 additions & 139 deletions

File tree

CLAUDE.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,14 @@ vgi-client --input data.parquet --function sum_all_columns --server vgi-example-
113113
import pyarrow as pa
114114
import pyarrow.compute as pc
115115
from vgi import ScalarFunction, Arg
116+
from vgi.arguments import AnyArrow
116117

117118
class DoubleColumn(ScalarFunction):
118119
"""Double the value in a specified column."""
119120

121+
class Meta:
122+
output_type = AnyArrow # Output type depends on input column
123+
120124
column = Arg[str](0, doc="Column to double")
121125

122126
@property
@@ -329,17 +333,16 @@ At bind time:
329333
class AddColumns(ScalarFunction):
330334
"""Add two numeric columns with dynamic output type."""
331335

336+
class Meta:
337+
output_type = AnyArrow # Output type depends on input columns
338+
332339
col1 = Arg[AnyArrow](0, type_bound=pa.types.is_numeric)
333340
col2 = Arg[AnyArrow](1, type_bound=pa.types.is_numeric)
334341

335342
def bind(self) -> None:
336343
"""Compute output type from input columns."""
337344
self._output_type = self.input_schema.field(self.col1.value).type
338345

339-
@classmethod
340-
def catalog_output_type(cls) -> pa.DataType | type[AnyArrow]:
341-
return AnyArrow
342-
343346
@property
344347
def output_type(self) -> pa.DataType:
345348
return self._output_type
@@ -375,10 +378,11 @@ For advanced distributed aggregations with `max_workers > 1`, use `store_state()
375378

376379
**ScalarFunction:**
377380

378-
| Method | When to Override | Default |
379-
|--------|------------------|---------|
381+
| Method/Attribute | When to Override | Default |
382+
|------------------|------------------|---------|
383+
| `Meta.output_type` | Always required (pa.DataType or AnyArrow) | Required |
380384
| `bind()` | Process input schema, compute dynamic output type | No-op |
381-
| `output_type` | Define output column type | Required |
385+
| `output_type` | Override if Meta.output_type is AnyArrow | Uses Meta.output_type |
382386
| `compute(batch)` | Transform batch to single array | Required |
383387
| `setup()` | Acquire resources | No-op |
384388
| `teardown()` | Release resources | No-op |
@@ -409,7 +413,7 @@ How will your function be used in SQL?
409413
410414
1. SELECT my_func(col1, col2) FROM table
411415
→ SCALAR FUNCTION: Returns one value per input row
412-
→ Use ScalarFunction, override output_type and compute()
416+
→ Use ScalarFunction, define Meta.output_type and compute()
413417
→ Example: upper(), abs(), concat()
414418
415419
2. SELECT * FROM my_func(args)

tests/scalar/test_function.py

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from tests.conftest import make_scalar_invocation
1212
from vgi import schema
13-
from vgi.arguments import AnyArrow, Arg, Arguments
13+
from vgi.arguments import Arg, Arguments
1414
from vgi.invocation import Invocation, InvocationType
1515
from vgi.log import Level, Message
1616
from vgi.scalar_function import (
@@ -139,11 +139,10 @@ def test_basic_compute(self) -> None:
139139
"""Test basic compute() method."""
140140

141141
class DoubleColumn(ScalarFunction):
142-
column = Arg[str](0)
142+
class Meta:
143+
output_type = pa.int64()
143144

144-
@classmethod
145-
def catalog_output_type(cls) -> pa.DataType | type[AnyArrow]:
146-
return pa.int64()
145+
column = Arg[str](0)
147146

148147
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
149148
import pyarrow.compute as pc
@@ -177,9 +176,8 @@ def test_log_method(self) -> None:
177176
"""Test self.log() method."""
178177

179178
class LoggingFunc(ScalarFunction):
180-
@classmethod
181-
def catalog_output_type(cls) -> pa.DataType | type[AnyArrow]:
182-
return pa.int64()
179+
class Meta:
180+
output_type = pa.int64()
183181

184182
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
185183
import pyarrow.compute as pc
@@ -211,9 +209,8 @@ def test_row_count_validation(self) -> None:
211209
"""Test that row count mismatch raises error."""
212210

213211
class WrongRowCount(ScalarFunction):
214-
@classmethod
215-
def catalog_output_type(cls) -> pa.DataType | type[AnyArrow]:
216-
return pa.int64()
212+
class Meta:
213+
output_type = pa.int64()
217214

218215
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
219216
# Return wrong number of rows
@@ -238,9 +235,8 @@ def test_row_count_exceeds_input(self) -> None:
238235
"""Test that output with more rows than input raises error (lines 134-142)."""
239236

240237
class TooManyRows(ScalarFunction):
241-
@classmethod
242-
def catalog_output_type(cls) -> pa.DataType | type[AnyArrow]:
243-
return pa.int64()
238+
class Meta:
239+
output_type = pa.int64()
244240

245241
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
246242
# Return MORE rows than input (expanding rows is not allowed)
@@ -267,9 +263,8 @@ def test_empty_batch(self) -> None:
267263
"""Test handling of empty batches."""
268264

269265
class DoubleFunc(ScalarFunction):
270-
@classmethod
271-
def catalog_output_type(cls) -> pa.DataType | type[AnyArrow]:
272-
return pa.int64()
266+
class Meta:
267+
output_type = pa.int64()
273268

274269
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
275270
import pyarrow.compute as pc

tests/test_type_bounds.py

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,10 @@ def test_type_bound_passes_for_valid_type(self) -> None:
3838
"""Type bound validation should pass when predicate returns True."""
3939

4040
class TestFunc(ScalarFunction):
41-
col = Arg[AnyArrow](0, type_bound=pa.types.is_integer)
41+
class Meta:
42+
output_type = pa.int64()
4243

43-
@classmethod
44-
def catalog_output_type(cls) -> pa.DataType:
45-
return pa.int64()
44+
col = Arg[AnyArrow](0, type_bound=pa.types.is_integer)
4645

4746
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
4847
return batch.column(self.col.value)
@@ -58,11 +57,10 @@ def test_type_bound_fails_for_invalid_type(self) -> None:
5857
"""Type bound validation should raise when predicate returns False."""
5958

6059
class TestFunc(ScalarFunction):
61-
col = Arg[AnyArrow](0, type_bound=pa.types.is_integer)
60+
class Meta:
61+
output_type = pa.int64()
6262

63-
@classmethod
64-
def catalog_output_type(cls) -> pa.DataType:
65-
return pa.int64()
63+
col = Arg[AnyArrow](0, type_bound=pa.types.is_integer)
6664

6765
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
6866
return batch.column(self.col.value)
@@ -78,14 +76,13 @@ def test_multiple_type_bounds_or_logic_passes(self) -> None:
7876
"""When multiple predicates are given, any match should pass (OR logic)."""
7977

8078
class TestFunc(ScalarFunction):
79+
class Meta:
80+
output_type = pa.float64()
81+
8182
col = Arg[AnyArrow](
8283
0, type_bound=[pa.types.is_integer, pa.types.is_floating]
8384
)
8485

85-
@classmethod
86-
def catalog_output_type(cls) -> pa.DataType:
87-
return pa.float64()
88-
8986
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
9087
return batch.column(self.col.value)
9188

@@ -100,14 +97,13 @@ def test_multiple_type_bounds_or_logic_fails(self) -> None:
10097
"""When multiple predicates fail, validation should raise."""
10198

10299
class TestFunc(ScalarFunction):
100+
class Meta:
101+
output_type = pa.float64()
102+
103103
col = Arg[AnyArrow](
104104
0, type_bound=[pa.types.is_integer, pa.types.is_floating]
105105
)
106106

107-
@classmethod
108-
def catalog_output_type(cls) -> pa.DataType:
109-
return pa.float64()
110-
111107
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
112108
return batch.column(self.col.value)
113109

@@ -123,11 +119,10 @@ def test_type_bound_on_non_anyarrow_warns(self) -> None:
123119
with pytest.warns(UserWarning, match="only meaningful for Arg\\[AnyArrow\\]"):
124120

125121
class TestFunc(ScalarFunction):
126-
col = Arg[str](0, type_bound=pa.types.is_integer)
122+
class Meta:
123+
output_type = pa.string()
127124

128-
@classmethod
129-
def catalog_output_type(cls) -> pa.DataType:
130-
return pa.string()
125+
col = Arg[str](0, type_bound=pa.types.is_integer)
131126

132127
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
133128
return batch.column(self.col)
@@ -174,11 +169,10 @@ def test_error_message_includes_context(self) -> None:
174169
"""Error messages should include argument name and predicate names."""
175170

176171
class TestFunc(ScalarFunction):
177-
my_column = Arg[AnyArrow](0, type_bound=pa.types.is_integer)
172+
class Meta:
173+
output_type = pa.int64()
178174

179-
@classmethod
180-
def catalog_output_type(cls) -> pa.DataType:
181-
return pa.int64()
175+
my_column = Arg[AnyArrow](0, type_bound=pa.types.is_integer)
182176

183177
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
184178
return batch.column(self.my_column.value)
@@ -205,11 +199,10 @@ def is_large_int(dtype: pa.DataType) -> bool:
205199
return dtype in (pa.int64(), pa.uint64())
206200

207201
class TestFunc(ScalarFunction):
208-
col = Arg[AnyArrow](0, type_bound=is_large_int)
202+
class Meta:
203+
output_type = pa.int64()
209204

210-
@classmethod
211-
def catalog_output_type(cls) -> pa.DataType:
212-
return pa.int64()
205+
col = Arg[AnyArrow](0, type_bound=is_large_int)
213206

214207
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
215208
return batch.column(self.col.value)
@@ -230,11 +223,10 @@ def test_type_bound_with_lambda(self) -> None:
230223
"""Lambda predicates should work and show in error messages."""
231224

232225
class TestFunc(ScalarFunction):
233-
col = Arg[AnyArrow](0, type_bound=lambda t: pa.types.is_integer(t))
226+
class Meta:
227+
output_type = pa.int64()
234228

235-
@classmethod
236-
def catalog_output_type(cls) -> pa.DataType:
237-
return pa.int64()
229+
col = Arg[AnyArrow](0, type_bound=lambda t: pa.types.is_integer(t))
238230

239231
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
240232
return batch.column(self.col.value)
@@ -261,11 +253,10 @@ def test_varargs_type_bound_passes_for_all_valid_types(self) -> None:
261253
"""Type bound validation passes when all varargs elements are valid."""
262254

263255
class TestFunc(ScalarFunction):
264-
columns = Arg[AnyArrow](0, varargs=True, type_bound=pa.types.is_integer)
256+
class Meta:
257+
output_type = pa.int64()
265258

266-
@classmethod
267-
def catalog_output_type(cls) -> pa.DataType:
268-
return pa.int64()
259+
columns = Arg[AnyArrow](0, varargs=True, type_bound=pa.types.is_integer)
269260

270261
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
271262
# Sum all integer columns (varargs returns tuple at runtime)
@@ -288,11 +279,10 @@ def test_varargs_type_bound_fails_when_any_element_invalid(self) -> None:
288279
"""Type bound validation should fail if any varargs element has invalid type."""
289280

290281
class TestFunc(ScalarFunction):
291-
columns = Arg[AnyArrow](0, varargs=True, type_bound=pa.types.is_integer)
282+
class Meta:
283+
output_type = pa.int64()
292284

293-
@classmethod
294-
def catalog_output_type(cls) -> pa.DataType:
295-
return pa.int64()
285+
columns = Arg[AnyArrow](0, varargs=True, type_bound=pa.types.is_integer)
296286

297287
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
298288
result = batch.column(self.columns[0]) # type: ignore[index]
@@ -313,14 +303,13 @@ def test_varargs_type_bound_with_multiple_predicates(self) -> None:
313303
"""Varargs with multiple type bounds should use OR logic per element."""
314304

315305
class TestFunc(ScalarFunction):
306+
class Meta:
307+
output_type = pa.float64()
308+
316309
columns = Arg[AnyArrow](
317310
0, varargs=True, type_bound=[pa.types.is_integer, pa.types.is_floating]
318311
)
319312

320-
@classmethod
321-
def catalog_output_type(cls) -> pa.DataType:
322-
return pa.float64()
323-
324313
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
325314
return batch.column(self.columns[0]) # type: ignore[index]
326315

vgi/examples/scalar.py

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class Meta:
4545

4646
name = "double_column"
4747
description = "Doubles values in a numeric column"
48+
output_type = AnyArrow # Output type depends on input column type
4849
examples = [
4950
FunctionExample(
5051
sql="SELECT double_column(price) FROM products",
@@ -59,11 +60,6 @@ class Meta:
5960
# Explicit arrow_type demonstrates type specification
6061
column = Arg[str](0, doc="Column name to double", arrow_type=pa.utf8())
6162

62-
@classmethod
63-
def catalog_output_type(cls) -> pa.DataType | type[AnyArrow]:
64-
"""Output type depends on input column type."""
65-
return AnyArrow
66-
6763
@property
6864
def output_type(self) -> pa.DataType:
6965
"""Return the type of the doubled column."""
@@ -134,6 +130,7 @@ class Meta:
134130

135131
name = "add_columns"
136132
description = "Adds two numeric columns"
133+
output_type = AnyArrow # Output type depends on input column types
137134
examples = [
138135
FunctionExample(
139136
sql="SELECT add_columns(price, tax) FROM orders",
@@ -165,11 +162,6 @@ def bind(self) -> None:
165162
).type
166163
self._output_type = _promote_for_addition(common_type)
167164

168-
@classmethod
169-
def catalog_output_type(cls) -> pa.DataType | type[AnyArrow]:
170-
"""Output type depends on input column types."""
171-
return AnyArrow
172-
173165
@property
174166
def output_type(self) -> pa.DataType:
175167
"""Return the computed output type based on input column types."""
@@ -195,6 +187,7 @@ class Meta:
195187

196188
name = "upper_case"
197189
description = "Converts string column to uppercase"
190+
output_type = pa.string() # Static output type
198191
examples = [
199192
FunctionExample(
200193
sql="SELECT upper_case(name) FROM users",
@@ -208,12 +201,7 @@ class Meta:
208201

209202
column = Arg[str](0, doc="Column name to uppercase")
210203

211-
@classmethod
212-
def catalog_output_type(cls) -> pa.DataType | type[AnyArrow]:
213-
"""Return string type (static output)."""
214-
return pa.string()
215-
216-
# Note: No need to override output_type - default uses catalog_output_type()
204+
# Note: No need to override output_type - default uses Meta.output_type
217205

218206
def compute(self, batch: pa.RecordBatch) -> pa.Array[Any]:
219207
"""Convert the column values to uppercase."""
@@ -238,6 +226,7 @@ class Meta:
238226

239227
name = "sum_columns"
240228
description = "Sum values from multiple numeric columns"
229+
output_type = AnyArrow # Output type depends on input column types
241230
examples = [
242231
FunctionExample(
243232
sql="SELECT sum_columns(price, tax, shipping) FROM orders",
@@ -262,11 +251,6 @@ def bind(self) -> None:
262251
first_type = self.input_schema.field(first_col).type
263252
self._output_type = _promote_for_addition(first_type)
264253

265-
@classmethod
266-
def catalog_output_type(cls) -> pa.DataType | type[AnyArrow]:
267-
"""Output type depends on input column types."""
268-
return AnyArrow
269-
270254
@property
271255
def output_type(self) -> pa.DataType:
272256
"""Return the computed output type based on first column."""

vgi/metadata.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,7 @@ def _normalize_examples(
667667
"distinct_dependent",
668668
# Scalar function specific
669669
"return_type",
670+
"output_type", # pa.DataType | type[AnyArrow] for scalar functions
670671
}
671672
)
672673

0 commit comments

Comments
 (0)