What's new
vgi-python has moved from 0.8.0 to 0.28.1, and the wire protocol from 1.0.0 to 1.3.0. This page groups what landed by theme rather than by release, and links to the guide for each. Two changes are breaking — both are called out below.
Breaking changes
Section titled “Breaking changes”A function name was never a unique key. The worker’s registry was a flat name → classes map, so two
functions registered under the same name in different catalog schemas collided as overloads and any
call raised “Ambiguous function call”. Registration was already schema-aware; dispatch was not.
BindRequest gained schema_name, and all five Client entry points now require it
(vgi-client gained --schema). Callers that pass no schema search every schema and get a
cross-schema ambiguity error naming the schemas involved. Functions from the legacy flat functions
list register into the catalog’s default_schema. Protocol 1.0.0 → 1.1.0.
The follow-on. 1.1.0 fixed bind, but the unary RPCs that re-resolve a function by name carried
no schema — so a name declared in two schemas bound correctly and then ran the other schema’s
implementation at update/finalize/process, returning a wrong-but-plausible answer.
A nullable schema_name was added to the 15 request dataclasses that resolve by name (five aggregate
core, four window, three streaming, three table-buffering) and threaded to every by-name resolution
site. Protocol 1.1.0 → 1.2.0.
Nothing else in this range requires a source change.
Result caching
Section titled “Result caching”New in 0.14.0, and the largest addition in this range. A worker can advertise that its result is
reusable by attaching vgi.cache.* metadata to its first data batch — a freshness lifetime, a reuse
scope, validators for conditional revalidation, and stale-serving grace windows, with the vocabulary
borrowed from HTTP caching.
| Version | What landed |
|---|---|
| 0.14.0 | CacheControl and the vgi.cache.* keys for table functions. |
| 0.16.0 | Cache control from a buffering function’s finalize emit; conditional-revalidation validators surfaced to the worker (params.if_none_match / if_modified_since). |
| 0.16.1 | Opt-in for scalar functions; partition_scope for per-partition caching. |
| 0.18.0 | per_value — per-VALUE memoization for exchange-mode maps. |
Custom COPY formats
Section titled “Custom COPY formats”A catalog can register its own COPY formats, in both directions (0.8.8). A CopyFromFunction
is an ordinary producer-mode table function; a CopyToFunction is a buffered function with no Source
phase. 0.9.0 added the on_secrets hook, which forwards CREATE SECRET credentials to a writer
through a two-phase bind retry — the seam that makes writing to s3:// possible when on_bind is
@final.
Function shapes
Section titled “Function shapes”- Blended table-in-out (
RowTransformFunction, 0.16.0) — positional args are the per-row input columns, so one registration servesf(52, 13),FROM t, f(t.x, t.y)and correlatedLATERAL. Map-shaped only: there is nofinalize, because DuckDB forbidsFinalExecuteunder correlatedLATERAL. Overloads resolve by arity, and 0.16.0 also added per-output-row provenance so batched correlatedLATERALstays correct. - Parallel finalize /
substream_id(0.16.0) for table-in-out functions. - Ordered buffering input — not new (
Meta.requires_input_batch_indexshipped in 0.8.0) but newly documented, with a runnable example worker as of 0.28.0. It delivers DuckDB’s per-chunk index alongside each buffered batch, so a parallel sink can reconstruct source order; when a source can’t supply one (range(),VALUES), the extension serializes the sink and numbers the batches itself.
Catalogs
Section titled “Catalogs”| Version | What landed |
|---|---|
| 0.8.4 | Declarative Catalog can advertise source_url. |
| 0.10.0 | Companion catalogs (attach_catalogs) — the client ATTACHes a companion lakehouse (DuckLake / Iceberg / Postgres) at VGI-attach time, so catalog-table branches resolve without hand-attaching. Scheme allowlist + never-clobber conflict policy. |
| 0.15.0 | required_field_filter_paths → required_filters, now AND-of-ORs (conjunctive normal form) with prefix-based satisfaction. |
| 0.15.0 | Table.arguments forwarded to the scan-time worker bind. |
| 0.24.0 | Global functions — publish selected functions into DuckDB’s system.main so they’re callable unqualified. Best-effort and advisory; first attach wins. Protocol 1.2.0 → 1.3.0. |
| 0.28.0 | deserialize_column_statistics() and Client.table_column_statistics() — the missing inverse of serialize_column_statistics. |
→ Expose a catalog · Publish global functions
Arguments
Section titled “Arguments”TaggedUnion(0.8.3) — union-typed arguments decode into a wrapper preserving both the discriminator and the value, instead ofas_py()silently dropping the tag. Extended to table varargs in 0.9.0.- Per-argument documentation (0.8.5) — serialized as the
vgi_docArrow field-metadata key. Extended to macro parameters in 0.8.6. - Typed const scalars (0.8.10) — a
ConstParamannotated as an Arrow scalar type is delivered as a typedpyarrow.Scalar. - Declared constraints (0.10.0) —
choices/ge/le/gt/lt/patternonParam/ConstParam, encoded into field metadata for discovery and enforced at bind for const args (ArgumentValidationError). ColumnarParamconstraints stay advisory.
→ Argument serialization · Function API
Serving and deployment
Section titled “Serving and deployment”| Version | What landed |
|---|---|
| 0.8.7 | TCP transport — vgi-serve –tcp, Client.from_tcp(). No auth or encryption: loopback and trusted networks only. |
| 0.22.1 | VGI-Proxy-Proof-Required advertised from create_app, so a proxy can confirm the worker enforces the proofs it mints. |
| 0.24.0 | Shared signing key — resolve_shared_signing_key() replaces a per-process os.urandom(32), and an ephemeral key is now announced at WARNING. See the warning below. |
| 0.24.0 | Optional granian server (–server granian) — Rust socket I/O off the GIL, measured 1.9x over the best waitress config from a single process. Plus the waitress tuning vgi-serve was missing. |
| 0.25.0 | –max-externalized-response-bytes (a hard cap, unlike the soft stream cap), Worker.resolve_token() for proxy token introspection, and access-log sampling. |
| 0.27.0 | The worker landing page reads the catalog over the protocol instead of producing a static describe.json. |
Before 0.24.0, an unset VGI_SIGNING_KEY minted a key per process — correct for one process and
silently wrong for more. The failure is load-dependent: a pinned connection never notices, while one
that reconnects mid-stream hits a token sealed under a key the receiving process doesn’t have and
gets an intermittent 400 that reads as flakiness.
Client
Section titled “Client”All from 0.28.0 / 0.28.1, and all about controlling a worker process properly:
stop(force=True)— SIGKILL a direct subprocess worker wedged inside a handler. A graceful stop blocks on exactly the case you want to escape. Inert for pooled workers (they belong to the pool, not the client); HTTP and TCP are already prompt.- Workers spawn without a shell, so the client owns the real process — which is what makes
forceactually kill it. server_pathaccepts an argv sequence, so an argument carrying spaces or quotes survives. No single quoting convention works on both POSIX and Windows, which is what the shell had been papering over.
State and storage
Section titled “State and storage”StreamStateCodec(0.24.0) — a stream function’s state no longer has to extendArrowSerializableDataclass. The framework only ever needed two methods, so the requirement is now a structurally-checked protocol. A two-integer state measured 416 bytes / 36µs through Arrow IPC against 16 bytes / 0.21µs packed directly — the framing was the entire cost.- Aggregates too (unreleased, after 0.28.1) — the same relaxation reached
AggregateFunction, whoseTStatehad still been bound toArrowSerializableDataclass. It matters more here than for stream functions: an aggregate serializes once per group, per batch, so the Arrow framing cost scales with cardinality. ATStatewith neither codec method is now rejected at class-definition time instead of failing as anAttributeErroron the first group of the first batch. function_storage(0.24.0) — writers are serialized on the shared-cache in-memory SQLite DB.
→ Persist state across workers
Performance and correctness
Section titled “Performance and correctness”- 0.16.1 —
statistics_from_duckdbcollects every column in a single scan instead of one pass per column, so cost no longer scales with table width. - 0.16.1 — filter pushdown drops a fully-degraded
ANDinstead of emittingWHERE (). - 0.21.0 — the embedded
bind_callserializes as binary, a cross-language wire format. - 0.23.0 — per-class argument specs are cached; worker logging and profiling became reachable
(
vgi.profiling, opt-in by environment variable, shared by every worker entry point). - 0.24.0 — HTTP call state moved out of the per-turn state token; waitress buffers sized for
Arrow bodies; the per-class
FunctionArgumentsparse plan is cached. - 0.24.0 — the attach is no longer carried decrypted in the call state.
Also worth knowing
Section titled “Also worth knowing”- Secrets (0.8.8) —
ResolvedSecretsbecame scope- and type-aware, and name-keyed. - Python 3.14 is tested in CI as of 0.25.0;
httpx2as of 0.26.0. - The
vgi-rpcfloor moved from 0.20.0 to 0.40.0 over this range; several capabilities above (TCP, externalized-response caps, proxy proofs) are VGI exposing what vgi-rpc had gained.
Next steps
Section titled “Next steps”- Start building → Function patterns.
- The full API → API Reference.
- Upstream changelog → Query-farm/vgi-python releases.