Use batch UDTFs for N:M transformations like deduplication, clustering, and aggregation across entire tables or partitions.
Beta — introduced in Geneva 0.11.0Geneva’s standard UDFs operate row-at-a-time — one input row produces exactly one output value. Batch User-Defined Table Functions (UDTFs) lift this restriction, enabling N:M transformations where the output can have a completely different schema and row count than the input.
Use the @udtf decorator on a class or function. The UDTF receives a query builder over the source data and yieldspa.RecordBatch objects with an arbitrary output schema.
On each refresh, Geneva checks the source table’s version against the version stored in the view metadata. If the source has not changed, the refresh is skipped entirely — an O(1) check.
When neither partition_by nor partition_by_indexed_column is set, the UDTF runs as a single Ray task with access to the entire source dataset. Use this for global operations that need cross-row visibility.
The framework groups source data by the partition column and dispatches each partition as an independent Ray task. Use this when the computation is naturally parallelizable by some grouping key.
Instead of partitioning by a materialized column, the framework reads partition assignments directly from an existing IVF vector index (IVF_FLAT, IVF_PQ, IVF_HNSW_FLAT, IVF_HNSW_SQ, etc.). This avoids materializing a partition_id column and keeps partitions synchronized with the index.
from geneva.partitioning import create_ivf_flat_index# 1. Build an IVF index on the source tablecreate_ivf_flat_index(images, "phash", k=16)# 2. Define the UDTF with index-based partitioning@udtf( output_schema=pa.schema([ ("row_id_a", pa.string()), ("row_id_b", pa.string()), ("hamming_dist", pa.int32()), ]), partition_by_indexed_column="phash", num_cpus=2,)class IndexPartitionedEdgeDetection: def __init__(self, threshold: int = 4): self.threshold = threshold def __call__(self, source: geneva.GenevaQueryBuilder) -> Iterator[pa.RecordBatch]: data = source.to_arrow() edges = self._pairwise_compare(data, self.threshold) if edges: yield pa.RecordBatch.from_pydict(edges)
partition_by
partition_by_indexed_column
Partition source
Column values (SQL filter)
IVF index partitions (row ID take)
Requires materialized column
Yes
No — reads from index metadata
Partition count
Number of distinct values
Number of non-empty index partitions
Sync with index
Manual
Automatic — always reads latest index
partition_by and partition_by_indexed_column are mutually exclusive. Setting both raises ValueError.
The UDTF yields one or more pa.RecordBatch or pa.Table objects. Each batch must conform to output_schema. The framework validates each batch, writes it, and optionally checkpoints it.
# Streaming — yield per source batch (memory-efficient)def __call__(self, source: geneva.GenevaQueryBuilder) -> Iterator[pa.RecordBatch]: for batch in source.to_batches(batch_size=1024): yield transform(batch)# Bulk — load all, compute, yield oncedef __call__(self, source: geneva.GenevaQueryBuilder) -> Iterator[pa.RecordBatch]: all_data = source.to_arrow() result = expensive_cross_row_computation(all_data) yield result.to_batches()[0]
Use the streaming pattern for memory-efficient processing. Use the bulk pattern when the computation inherently requires all data in memory (e.g., clustering, global deduplication).
Error handling operates at partition granularity — the unit of work is the entire __call__() execution for a partition (or the full table in single-worker mode).
Mode
Behavior
Use case
Fail (default)
Exception kills the partition, refresh fails
Correctness-critical UDTFs
Retry
Retry the entire partition with configurable backoff
Transient failures (network, OOM)
Skip
Log error, continue with remaining partitions
Best-effort / tolerant workloads
Unlike standard UDF error handling, there is no row-level skip — UDTFs yield whole batches, so the smallest error unit is the partition.
Each yielded batch is checkpointed before reporting completion. On resume after a failure, completed batches are skipped and entire partitions with a __done__ marker are skipped.Checkpoint keys include the source table version, so stale checkpoints from a previous source version are automatically ignored when the source changes.
Checkpointed UDTFs must be deterministic — the same input must yield the same batch sequence for resume to work correctly.