Skip to content

Commit c98c74b

Browse files
rustyconoverclaude
andcommitted
test(metadata): cover arbitrary function tags flowing into FunctionInfo
Verifies a worker can attach arbitrary key/value tags to a function via Meta.tags (e.g. vgi.columns_md for VGI307), and that they flow through resolve_metadata -> ResolvedMetadata.tags -> FunctionInfo.tags (and the metadata Arrow wire roundtrip) to DuckDB/the linter. Also asserts the structured examples channel and a worker-set vgi.example_queries tag coexist without either clobbering the other. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3273c8b commit c98c74b

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

‎tests/test_metadata.py‎

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,3 +613,118 @@ class TestFunc(TableInOutFunction): # type: ignore[type-arg]
613613

614614
meta = resolve_metadata(TestFunc)
615615
assert meta.function_type == CatalogFunctionType.TABLE
616+
617+
618+
class TestFunctionTags:
619+
"""A worker can attach arbitrary free-form tags to a function's metadata.
620+
621+
These flow ``Meta.tags`` -> ``ResolvedMetadata.tags`` -> ``FunctionInfo.tags``
622+
-> DuckDB's per-function ``tags`` map, which is what the ``vgi-lint-check``
623+
metadata linter reads (e.g. VGI307 requires ``vgi.columns_md`` on a
624+
dynamic-schema table function).
625+
"""
626+
627+
def test_meta_tags_resolved(self) -> None:
628+
"""Arbitrary Meta.tags land on ResolvedMetadata.tags verbatim."""
629+
630+
class TaggedFunction(TableInOutFunction): # type: ignore[type-arg]
631+
class Meta:
632+
name = "tagged_func"
633+
tags = {
634+
"vgi.columns_md": "| col | type |\n| --- | --- |\n| id | BIGINT |",
635+
"custom.team": "data",
636+
}
637+
638+
data: TableInput = Arg[TableInput](0, doc="Input table") # type: ignore[assignment]
639+
640+
meta = resolve_metadata(TaggedFunction)
641+
assert meta.tags == {
642+
"vgi.columns_md": "| col | type |\n| --- | --- |\n| id | BIGINT |",
643+
"custom.team": "data",
644+
}
645+
646+
def test_tags_survive_arrow_roundtrip(self) -> None:
647+
"""Function tags survive the metadata Arrow wire roundtrip (map column)."""
648+
649+
class TaggedFunction(TableInOutFunction): # type: ignore[type-arg]
650+
class Meta:
651+
name = "tagged_func"
652+
tags = {"vgi.columns_md": "| col |", "k": "v"}
653+
654+
data: TableInput = Arg[TableInput](0, doc="Input table") # type: ignore[assignment]
655+
656+
restored = arrow_to_functions(functions_to_arrow([TaggedFunction]))[0]
657+
assert restored.tags == {"vgi.columns_md": "| col |", "k": "v"}
658+
659+
def test_tags_and_examples_are_independent(self) -> None:
660+
"""Worker tags do not clobber the structured examples, and vice versa.
661+
662+
``examples`` is its own structured channel (serialized to the
663+
``examples`` struct column / ``FunctionInfo.examples``); it is never
664+
folded into the ``tags`` map. A worker that *also* sets a
665+
``vgi.example_queries`` tag (the tag-encoded examples convention the
666+
linter understands) keeps both — the tag is preserved untouched and the
667+
structured examples remain separate.
668+
"""
669+
670+
class BothFunction(TableInOutFunction): # type: ignore[type-arg]
671+
class Meta:
672+
name = "both_func"
673+
examples = ["SELECT * FROM both_func((SELECT 1))"]
674+
tags = {
675+
"vgi.example_queries": '[{"description":"x","sql":"SELECT tag"}]',
676+
"vgi.columns_md": "| col |",
677+
}
678+
679+
data: TableInput = Arg[TableInput](0, doc="Input table") # type: ignore[assignment]
680+
681+
meta = resolve_metadata(BothFunction)
682+
# Worker-provided tags untouched (example_queries tag not derived, not dropped).
683+
assert meta.tags == {
684+
"vgi.example_queries": '[{"description":"x","sql":"SELECT tag"}]',
685+
"vgi.columns_md": "| col |",
686+
}
687+
# Structured examples live independently and are not merged into tags.
688+
assert [ex.sql for ex in meta.examples] == ["SELECT * FROM both_func((SELECT 1))"]
689+
690+
def test_tags_flow_into_function_info(self) -> None:
691+
"""Tags reach ``FunctionInfo.tags`` (catalog -> DuckDB -> linter path).
692+
693+
Also asserts that the structured ``examples`` and the worker-set
694+
``vgi.example_queries`` tag coexist on ``FunctionInfo`` without either
695+
clobbering the other.
696+
"""
697+
from vgi.catalog.catalog_interface import (
698+
FunctionInfo,
699+
ReadOnlyCatalogInterface,
700+
)
701+
from vgi.table_function import TableFunctionGenerator
702+
703+
class DynamicTable(TableFunctionGenerator): # type: ignore[type-arg]
704+
class Meta:
705+
name = "dynamic_table"
706+
examples = ["SELECT * FROM dynamic_table()"]
707+
tags = {
708+
"vgi.columns_md": "| col | type |\n| --- | --- |\n| id | BIGINT |",
709+
"vgi.example_queries": '[{"description":"x","sql":"SELECT 1"}]',
710+
"custom.team": "data",
711+
}
712+
713+
FIXED_SCHEMA = pa.schema([pa.field("id", pa.int64())])
714+
715+
def process(self, params): # type: ignore[no-untyped-def]
716+
yield pa.record_batch({"id": [1]})
717+
718+
class Cat(ReadOnlyCatalogInterface):
719+
catalog_name = "functions"
720+
functions = [DynamicTable]
721+
722+
info = Cat()._function_to_info(DynamicTable, "main")
723+
assert isinstance(info, FunctionInfo)
724+
assert info.tags == {
725+
"vgi.columns_md": "| col | type |\n| --- | --- |\n| id | BIGINT |",
726+
"vgi.example_queries": '[{"description":"x","sql":"SELECT 1"}]',
727+
"custom.team": "data",
728+
}
729+
# Structured examples are carried separately from the tag-encoded ones.
730+
assert [ex.sql for ex in info.examples] == ["SELECT * FROM dynamic_table()"]

0 commit comments

Comments
 (0)