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.
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.
Step 1 — Write the worker
Section titled “Step 1 — Write the worker”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:
# /// 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.
valueis apa.Int64Arrayvector, not one row — vectorized over Apache Arrow. - Types are the schema. VGI reads the
Annotated[…]hints to derivedouble(BIGINT) → BIGINT— no separate registration.
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.
Step 2 — Start Haybarn
Section titled “Step 2 — Start Haybarn”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 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.
They’re easy to mix up:
vgi-python— the Python package you built the worker with in Step 1 (installed byuv/pip).- the
vgiengine extension — the engine-side piece that lets DuckDB launch and talk to your worker (theTYPE vgiinATTACH). Haybarn ships it; you load it withINSTALL vgi FROM community; LOAD vgi;in the next step.
Step 3 — Attach and call it
Section titled “Step 3 — Attach and call it”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.
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. 🎉
TroubleshootingIO Error: VGI worker not found or not executable— theLOCATIONcommand 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 thePATHof the shell that started the engine. Install it withcurl -LsSf https://astral.sh/uv/install.sh | shand open a fresh terminal — or skip uv entirely (see Prefer not to use uv? above).- The
ATTACHhangs — runuv run calc_scalar_worker.pydirectly; 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 (Double→double), qualified by the catalog name fromATTACH.Catalog Error: unknown type "vgi"— the extension isn’t loaded. Haybarn shipsvgiand loads it on demand, so you shouldn’t see this here; on stock DuckDB you would, and it can’t be fixed withINSTALL(see Why Haybarn? above).
Next: 2. Add a table function → — one worker, two functions.