Skip to content
Query.Farm
Talk with Us

Function metadata

How a function describes itself. A nested Meta class sets the SQL name, the human-readable description, examples, and the behavioural flags DuckDB’s planner reads — parallelism, order preservation, pushdown support. Every value has a sensible default, so Meta is optional and you set only what you need. No inheritance, no registration call.

Meta works on every shape — ScalarFunction, TableFunctionGenerator, TableInOutFunction, AggregateFunction, TableBufferingFunction. Set the attributes you care about:

from typing import Annotated
from vgi import ScalarFunction, Param, ConstParam, Returns
import pyarrow as pa
import pyarrow.compute as pc

class MultiplyFunction(ScalarFunction):
  """Multiplies a value by a constant factor."""

  class Meta:
      name = "multiply"
      description = "Multiplies a value by a constant factor"
      categories = ["numeric", "transform"]

  @classmethod
  def compute(
      cls,
      value: Annotated[pa.Int64Array, Param(doc="Integer value to multiply")],
      factor: Annotated[int, ConstParam("Multiplication factor")],
  ) -> Annotated[pa.Int64Array, Returns()]:
      return pc.multiply(value, factor)

That registers as multiply(BIGINT, BIGINT) → BIGINT. Two defaults are worth knowing because they mean you can often omit Meta entirely:

  • name defaults to the class name in snake_case — MultiplyFunction would have registered as multiply_function without the override.
  • description defaults to the first line of the class docstring, so the docstring above would have served on its own.

Every function class can report its own resolved metadata — after defaults, inheritance, and signature introspection have been applied:

meta = MultiplyFunction.get_metadata()
print(meta.name)         # "multiply"
print(meta.max_workers)  # None (unbounded)
print(meta.parameters)   # [ParameterInfo(name='value', ...), ParameterInfo(name='factor', ...)]

info = MultiplyFunction.describe()   # the same thing as a JSON-serializable dict

get_metadata() is what the framework itself calls when it builds the registration a client sees, so it’s the authoritative answer to “what did I actually declare?” — including the parameter list derived from your compute() annotations, which you never wrote out by hand.

AttributeTypeDefaultDescription
namestrClass name → snake_caseFunction registration name
descriptionstrFirst docstring lineHuman-readable description
categorieslist[str][]Classification tags
tagsdict[str, str]{}Custom key-value tags, surfaced on FunctionInfo
exampleslist[]SQL examples (str or FunctionExample)
max_workersint|NoneNone (unlimited)Max parallel workers
stabilityFunctionStabilityCONSISTENTOutput determinism
null_handlingNullHandlingDEFAULTNULL input behavior
required_settingslist[str][]Required DuckDB settings
projection_pushdownboolTrueEnable column pruning
filter_pushdownboolFalseEnable filter pushdown
preserves_orderOrderPreservationPRESERVES_ORDERRow order guarantee
order_dependentOrderDependenceNOT_ORDER_DEPENDENTAggregate order sensitivity
distinct_dependentDistinctDependenceNOT_DISTINCT_DEPENDENTAggregate DISTINCT sensitivity
supports_windowboolFalseAggregate implements the window() callback
streaming_partitionedboolFalseAggregate opts into the streaming-partitioned protocol
output_typepa.DataType|AnyArrowRequired for ScalarFunctionScalar output type

These are meaningful only on a TableBufferingFunction, and govern how the sink and source phases are scheduled:

AttributeDefaultDescription
requires_input_batch_indexFalseEach process() call carries a globally-unique monotonic params.batch_index, so a parallel sink can reconstruct source order — see Recovering input order. Mutually exclusive with sink_order_dependent.
sink_order_dependentFalseRun the sink single-threaded so every process() call arrives in source order. The blunt alternative to the batch index — simpler, but it gives up sink parallelism.
source_order_dependentFalseRun the source phase single-threaded, draining finalize_state_ids in combine-returned order.
Some metadata is derived, not declared

has_finalize (whether you overrode finalize()/finish()) and input_from_args (whether you subclassed RowTransformFunction) are resolved from the class itself, not set in Meta. The C++ extension reads both to decide how to register the function, which is exactly why they can’t be hand-declared and get out of sync.

Meta attributes are inherited from parent classes, so a family of related functions can share a base:

class FilterFunction(TableInOutFunction):
  class Meta:
      categories = ["filter"]
      preserves_order = OrderPreservation.PRESERVES_ORDER

class PositiveFilter(FilterFunction):
  class Meta:
      description = "Keep only positive values"
  # Inherits categories=["filter"] and preserves_order from the parent

Metadata crosses the wire as Arrow — this is how a client learns what a worker offers. You rarely call these directly (the worker does it during attach), but they’re the seam if you’re building tooling:

from vgi import functions_to_arrow
from vgi.metadata import arrow_to_functions

# Worker side: describe the available functions as a RecordBatch.
batch = functions_to_arrow([MultiplyFunction, PositiveFilter])

# Client side: read them back.
for info in arrow_to_functions(batch):
  print(f"{info.name}: {info.description}")

Per-argument documentation and constraints travel separately, as Arrow field metadata on the argument schema — see Argument serialization → Discovery metadata.