Skip to content
Query.Farm
Talk with Us

1. Your first scalar function

The first tutorial step: build a worker with one scalar function and call it from SQL — about 10 minutes, for first-time VGI users with Python 3.13+ and uv.

What's a “worker”?

A worker is a small Python program DuckDB launches as a subprocess and talks to over Apache Arrow. It exposes one or more typed functions, and DuckDB calls them like built-ins.

scalar shape
1 row → 1 value

Runs on each row independently and returns a single value — a pure per-row transform. This is the first of the five function shapes.

Why this is powerful

double is intentionally trivial — DuckDB can already do n * 2. The point is that compute is ordinary Python: drop in a NumPy routine, a machine-learning model, a regex DuckDB’s dialect lacks, or an HTTP API, and DuckDB calls it like a native SQL function.

Create calc_scalar_worker.py. The Double class is the whole function; the rest wraps it in a worker that publishes the calc catalog, with a # /// script header so uv run installs vgi-python with no setup:

calc_scalar_worker.py
# /// script
# requires-python = ">=3.13"
# dependencies = ["vgi-python"]
# ///
"""Stage 1 of the tutorial: a worker with a single scalar function.

Run it from a DuckDB-compatible engine (Haybarn shown here)::

    uvx haybarn-cli
    ATTACH 'calc' (TYPE vgi, LOCATION 'uv run calc_scalar_worker.py');
    SELECT calc.double(21);   -- 42
"""

from typing import Annotated

import pyarrow as pa
import pyarrow.compute as pc

from vgi import Param, Returns, ScalarFunction, Worker
from vgi.catalog import Catalog, Schema


class Double(ScalarFunction):
    """Double each input value (one row in, one row out)."""

    @classmethod
    def compute(
        cls,
        value: Annotated[pa.Int64Array, Param(doc="Values to double")],
    ) -> Annotated[pa.Int64Array, Returns()]:
        """Multiply the whole column by 2."""
        return pc.multiply(value, 2)


class CalcWorker(Worker):
    """A worker exposing the ``calc`` catalog with one scalar function."""

    catalog = Catalog(
        name="calc",
        schemas=[Schema(name="main", functions=[Double])],
    )


if __name__ == "__main__":
    CalcWorker().run()

Three things to notice about Double:

  • Any Python. The body is pc.multiply(value, 2) today; tomorrow it’s a model or an API call.
  • A whole column at once. value is a pa.Int64Array vector, not one row — vectorized over Apache Arrow.
  • Types are the schema. VGI reads the Annotated[…] hints to derive double(BIGINT) → BIGINT — no separate registration.
New to Apache Arrow?

Apache Arrow is a language-independent columnar memory format. Rather than rows of objects, data lives in arrays — also called vectors: a contiguous, typed sequence of values for a single column (here, a column of 64-bit integers). VGI hands your function a whole column as an Arrow array, not one value at a time, and operating on the whole array at once is what keeps it fast — if you’ve written a DuckDB UDF before, this is the vectorized equivalent.

In Python you work with these through PyArrow (imported as pa): pa.Int64Array is a column of int64 values, and pyarrow.compute (pc) provides vectorized operations like pc.multiply that run across the entire array. VGI moves these columns between DuckDB and your worker as Arrow record batches — a chunk of a table, i.e. a set of equal-length arrays.

VGI functions run inside a DuckDB-compatible engine. We’ll use Haybarn — start it from the folder holding calc_scalar_worker.py:

npx haybarn@rc

This opens Haybarn’s in-memory SQL shell — the memory H prompt — where the next step’s SQL goes. (First run downloads the CLI; see other install options.)

Haybarn serves the vgi extension

Haybarn distributes the vgi extension through its own channel, so INSTALL vgi FROM community; works out of the box (next step). vgi isn’t in DuckDB’s public community repository, so stock DuckDB can’t INSTALL it today — Haybarn is the supported path.

Two things are called “vgi”

They’re easy to mix up:

  • vgi-python — the Python package you built the worker with in Step 1 (installed by uv/pip).
  • the vgi engine extension — the engine-side piece that lets DuckDB launch and talk to your worker (the TYPE vgi in ATTACH). Haybarn ships it; you load it with INSTALL vgi FROM community; LOAD vgi; in the next step.

At the memory H prompt, load the vgi extension and attach the worker:

INSTALL vgi FROM community;
LOAD vgi;

ATTACH 'calc' (TYPE vgi, LOCATION 'uv run calc_scalar_worker.py');

The first ATTACH may pause for a few seconds while uv fetches vgi-python and PyArrow — that’s normal on a cold run, not a hang.

Run the worker without uv

LOCATION is just the command the engine runs to launch the worker — uv only reads the script’s inline # /// script header to auto-install vgi-python. To use plain Python instead, install the dependency yourself and point LOCATION at that interpreter:

python3 -m venv .venv
.venv/bin/pip install vgi-python
ATTACH 'calc' (TYPE vgi, LOCATION '.venv/bin/python calc_scalar_worker.py');

Any command that starts the worker with vgi-python importable works — uv run, a venv’s python, pipx run, or a system Python with the package installed.

Now call it:

SELECT calc.double(21);

Output

double(21)
42

…or over a whole column:

SELECT calc.double(n) FROM (VALUES (1), (2), (3)) AS t(n);

Output

double(n)
2
4
6

What just happened: ATTACH launched calc_scalar_worker.py as a subprocess and registered calc.double in your SQL session. DuckDB handed your Python the n column as a single Apache Arrow vector, compute ran on the whole thing at once, and the result streamed back — no row-by-row round trips. Swap pc.multiply for any Python you like and the SQL above doesn’t change. That’s the payoff: arbitrary Python, called like native SQL.

You’ve built and run your first VGI function. 🎉

Troubleshooting
  • IO Error: VGI worker not found or not executable — the LOCATION command can’t be run from the directory the engine was started in. Check the path and that the file exists.
  • uv: command not found — uv isn’t installed or isn’t on the PATH of the shell that started the engine. Install it with curl -LsSf https://astral.sh/uv/install.sh | sh and open a fresh terminal — or skip uv entirely (see Prefer not to use uv? above).
  • The ATTACH hangs — run uv run calc_scalar_worker.py directly; the worker speaks Arrow over stdin/stdout, so it looks like it hangs waiting for input. You’re checking for an import error on stderr.
  • Catalog Error: Scalar Function with name doubel does not exist! — check the spelling. The SQL name is the snake_case of the class name (Doubledouble), qualified by the catalog name from ATTACH.
  • Catalog Error: unknown type "vgi" — the extension isn’t loaded. Haybarn ships vgi and loads it on demand, so you shouldn’t see this here; on stock DuckDB you would, and it can’t be fixed with INSTALL (see Why Haybarn? above).

Next: 2. Add a table function → — one worker, two functions.