Skip to content

Commit 080a053

Browse files
rustyconoverclaude
andcommitted
Make FunctionInfo fields conditional on function type
- stability/null_handling: only set for scalar functions (None for table) - projection_pushdown/filter_pushdown/order_preservation: only set for table functions (None for scalar) Updated dataclass fields, ARROW_SCHEMA, serialize/deserialize methods, _function_to_info(), CLI output, and tests to reflect this behavior. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b0224a6 commit 080a053

3 files changed

Lines changed: 80 additions & 60 deletions

File tree

tests/catalog/test_catalog_interface.py

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -441,10 +441,7 @@ def test_default_values(self) -> None:
441441
from vgi.catalog import FunctionType
442442
from vgi.catalog.catalog_interface import (
443443
DistinctDependence,
444-
FunctionStability,
445-
NullHandling,
446444
OrderDependence,
447-
OrderPreservation,
448445
)
449446

450447
schema_bytes = self._get_empty_schema_bytes()
@@ -458,18 +455,18 @@ def test_default_values(self) -> None:
458455
tags={},
459456
)
460457

461-
# Behavior fields
462-
assert info.stability == FunctionStability.CONSISTENT
463-
assert info.null_handling == NullHandling.DEFAULT
458+
# Scalar behavior fields default to None (set by _function_to_info)
459+
assert info.stability is None
460+
assert info.null_handling is None
464461

465462
# Documentation fields
466463
assert info.examples == []
467464
assert info.categories == []
468465

469-
# Table function capabilities
470-
assert info.projection_pushdown is True
471-
assert info.filter_pushdown is False
472-
assert info.order_preservation == OrderPreservation.PRESERVES_ORDER
466+
# Table function capabilities default to None (set by _function_to_info)
467+
assert info.projection_pushdown is None
468+
assert info.filter_pushdown is None
469+
assert info.order_preservation is None
473470
assert info.max_workers is None
474471

475472
# Aggregate function fields
@@ -598,10 +595,7 @@ def test_backward_compatibility_without_new_fields(self) -> None:
598595
from vgi.catalog import FunctionInfo, FunctionType
599596
from vgi.catalog.catalog_interface import (
600597
DistinctDependence,
601-
FunctionStability,
602-
NullHandling,
603598
OrderDependence,
604-
OrderPreservation,
605599
)
606600

607601
# Create legacy schema without new fields
@@ -645,14 +639,14 @@ def test_backward_compatibility_without_new_fields(self) -> None:
645639
assert restored.comment == "A legacy function"
646640
assert restored.tags == {"version": "1.0"}
647641

648-
# New fields should have defaults
649-
assert restored.stability == FunctionStability.CONSISTENT
650-
assert restored.null_handling == NullHandling.DEFAULT
642+
# Optional fields should be None when not in legacy data
643+
assert restored.stability is None
644+
assert restored.null_handling is None
651645
assert restored.examples == []
652646
assert restored.categories == []
653-
assert restored.projection_pushdown is True
654-
assert restored.filter_pushdown is False
655-
assert restored.order_preservation == OrderPreservation.PRESERVES_ORDER
647+
assert restored.projection_pushdown is None
648+
assert restored.filter_pushdown is None
649+
assert restored.order_preservation is None
656650
assert restored.max_workers is None
657651
assert restored.order_dependent == OrderDependence.NOT_ORDER_DEPENDENT
658652
assert restored.distinct_dependent == DistinctDependence.NOT_DISTINCT_DEPENDENT

vgi/catalog/catalog_interface.py

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -347,18 +347,18 @@ class FunctionInfo(CatalogSchemaObject):
347347
# schema.serialize().to_pybytes()
348348
output_schema: SerializedSchema
349349

350-
# Behavior fields
351-
stability: FunctionStability = FunctionStability.CONSISTENT
352-
null_handling: NullHandling = NullHandling.DEFAULT
350+
# Scalar function behavior fields (None for non-scalar functions)
351+
stability: FunctionStability | None = None
352+
null_handling: NullHandling | None = None
353353

354354
# Documentation fields
355355
examples: list[str] = field(default_factory=list)
356356
categories: list[str] = field(default_factory=list)
357357

358-
# Table function capabilities
359-
projection_pushdown: bool = True
360-
filter_pushdown: bool = False
361-
order_preservation: OrderPreservation = OrderPreservation.PRESERVES_ORDER
358+
# Table function capabilities (None for scalar functions)
359+
projection_pushdown: bool | None = None
360+
filter_pushdown: bool | None = None
361+
order_preservation: OrderPreservation | None = None
362362
max_workers: int | None = None
363363

364364
# Aggregate function fields (future)
@@ -377,16 +377,16 @@ class FunctionInfo(CatalogSchemaObject):
377377
pa.field("output_schema", pa.binary(), nullable=False),
378378
pa.field("comment", pa.string(), nullable=True),
379379
pa.field("tags", pa.map_(pa.string(), pa.string()), nullable=False),
380-
# Behavior fields (enum values serialized as strings)
381-
pa.field("stability", pa.string(), nullable=False),
382-
pa.field("null_handling", pa.string(), nullable=False),
380+
# Scalar function behavior fields (nullable for non-scalar functions)
381+
pa.field("stability", pa.string(), nullable=True),
382+
pa.field("null_handling", pa.string(), nullable=True),
383383
# Documentation fields
384384
pa.field("examples", pa.list_(pa.string()), nullable=False),
385385
pa.field("categories", pa.list_(pa.string()), nullable=False),
386-
# Table function capabilities
387-
pa.field("projection_pushdown", pa.bool_(), nullable=False),
388-
pa.field("filter_pushdown", pa.bool_(), nullable=False),
389-
pa.field("order_preservation", pa.string(), nullable=False),
386+
# Table function capabilities (nullable for scalar functions)
387+
pa.field("projection_pushdown", pa.bool_(), nullable=True),
388+
pa.field("filter_pushdown", pa.bool_(), nullable=True),
389+
pa.field("order_preservation", pa.string(), nullable=True),
390390
pa.field("max_workers", pa.int32(), nullable=True),
391391
# Aggregate function fields
392392
pa.field("order_dependent", pa.string(), nullable=False),
@@ -408,16 +408,22 @@ def serialize(self) -> bytes:
408408
"output_schema": self.output_schema,
409409
"comment": self.comment,
410410
"tags": self.tags,
411-
# Behavior fields (enums serialized as name strings)
412-
"stability": self.stability.name,
413-
"null_handling": self.null_handling.name,
411+
# Scalar function behavior fields (None for non-scalar)
412+
"stability": self.stability.name if self.stability else None,
413+
"null_handling": (
414+
self.null_handling.name if self.null_handling else None
415+
),
414416
# Documentation fields
415417
"examples": self.examples,
416418
"categories": self.categories,
417-
# Table function capabilities
419+
# Table function capabilities (None for scalar)
418420
"projection_pushdown": self.projection_pushdown,
419421
"filter_pushdown": self.filter_pushdown,
420-
"order_preservation": self.order_preservation.name,
422+
"order_preservation": (
423+
self.order_preservation.name
424+
if self.order_preservation
425+
else None
426+
),
421427
"max_workers": self.max_workers,
422428
# Aggregate function fields
423429
"order_dependent": self.order_dependent.name,
@@ -457,18 +463,28 @@ def deserialize(cls, batch: pa.RecordBatch) -> Self:
457463
output_schema=SerializedSchema(row["output_schema"]),
458464
comment=row.get("comment"),
459465
tags=dict(row["tags"]) if row["tags"] else {},
460-
# Behavior fields (with backward-compatible defaults)
461-
stability=FunctionStability[row.get("stability", "CONSISTENT")],
462-
null_handling=NullHandling[row.get("null_handling", "DEFAULT")],
466+
# Scalar function behavior fields (None for non-scalar functions)
467+
stability=(
468+
FunctionStability[row["stability"]]
469+
if row.get("stability") is not None
470+
else None
471+
),
472+
null_handling=(
473+
NullHandling[row["null_handling"]]
474+
if row.get("null_handling") is not None
475+
else None
476+
),
463477
# Documentation fields
464478
examples=list(row.get("examples") or []),
465479
categories=list(row.get("categories") or []),
466-
# Table function capabilities
467-
projection_pushdown=row.get("projection_pushdown", True),
468-
filter_pushdown=row.get("filter_pushdown", False),
469-
order_preservation=OrderPreservation[
470-
row.get("order_preservation", "PRESERVES_ORDER")
471-
],
480+
# Table function capabilities (None for scalar functions)
481+
projection_pushdown=row.get("projection_pushdown"),
482+
filter_pushdown=row.get("filter_pushdown"),
483+
order_preservation=(
484+
OrderPreservation[row["order_preservation"]]
485+
if row.get("order_preservation") is not None
486+
else None
487+
),
472488
max_workers=row.get("max_workers"),
473489
# Aggregate function fields
474490
order_dependent=OrderDependence[
@@ -1149,6 +1165,8 @@ def _function_to_info(self, func_cls: type, schema_name: str) -> FunctionInfo:
11491165
output_schema = func_cls.catalog_output_schema() # type: ignore[attr-defined]
11501166
output_bytes = SerializedSchema(output_schema.serialize().to_pybytes())
11511167

1168+
is_scalar = func_type == FunctionType.SCALAR
1169+
11521170
return FunctionInfo(
11531171
name=meta.name,
11541172
schema_name=schema_name,
@@ -1157,17 +1175,17 @@ def _function_to_info(self, func_cls: type, schema_name: str) -> FunctionInfo:
11571175
output_schema=output_bytes,
11581176
comment=meta.description or None,
11591177
tags={},
1160-
# Behavior fields
1161-
stability=meta.stability,
1162-
null_handling=meta.null_handling,
1178+
# Scalar function behavior fields (None for non-scalar)
1179+
stability=meta.stability if is_scalar else None,
1180+
null_handling=meta.null_handling if is_scalar else None,
11631181
# Documentation fields
11641182
examples=[ex.sql for ex in meta.examples],
11651183
categories=meta.categories,
1166-
# Table function capabilities
1167-
projection_pushdown=meta.projection_pushdown,
1168-
filter_pushdown=meta.filter_pushdown,
1169-
order_preservation=meta.preserves_order,
1170-
max_workers=meta.max_workers,
1184+
# Table function capabilities (None for scalar)
1185+
projection_pushdown=None if is_scalar else meta.projection_pushdown,
1186+
filter_pushdown=None if is_scalar else meta.filter_pushdown,
1187+
order_preservation=None if is_scalar else meta.preserves_order,
1188+
max_workers=None if is_scalar else meta.max_workers,
11711189
# Aggregate function fields
11721190
order_dependent=meta.order_dependent,
11731191
distinct_dependent=meta.distinct_dependent,

vgi/client/cli_utils.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -299,16 +299,24 @@ def function_info_to_dict(function_info: Any) -> dict[str, Any]:
299299
"arguments": arrow_schema_to_json(function_info.arguments),
300300
"comment": function_info.comment,
301301
"tags": function_info.tags,
302-
# Behavior fields
303-
"stability": function_info.stability.name,
304-
"null_handling": function_info.null_handling.name,
302+
# Scalar function behavior fields (None for non-scalar)
303+
"stability": (
304+
function_info.stability.name if function_info.stability else None
305+
),
306+
"null_handling": (
307+
function_info.null_handling.name if function_info.null_handling else None
308+
),
305309
# Documentation fields
306310
"examples": function_info.examples,
307311
"categories": function_info.categories,
308-
# Table function capabilities
312+
# Table function capabilities (None for scalar)
309313
"projection_pushdown": function_info.projection_pushdown,
310314
"filter_pushdown": function_info.filter_pushdown,
311-
"order_preservation": function_info.order_preservation.name,
315+
"order_preservation": (
316+
function_info.order_preservation.name
317+
if function_info.order_preservation
318+
else None
319+
),
312320
"max_workers": function_info.max_workers,
313321
# Aggregate function fields
314322
"order_dependent": function_info.order_dependent.name,

0 commit comments

Comments
 (0)