Skip to content
Database workers

The database can carry the worker too.

Store a complete VGI executable or archive in a DuckDB table, move or snapshot that database like any other, then ATTACH the worker with a database:// location. VGI verifies, caches, and runs the package locally — no separate release download or container pull at query time.

A database-backed worker is still a local worker. What changes is delivery: instead of naming a command, release asset, or container image, the location names a row in a package registry. The bytes travel with the database; each attaching machine executes its own platform build.

From BLOB to process

How it works

01

Package

Store one executable or archive as a BLOB row, alongside its platform, version, entrypoint, and SHA-256.

02

Resolve

ATTACH selects exactly one row for the requested worker, current platform, and immutable package version.

03

Run locally

VGI verifies the bytes, materializes them in its private cache, and launches the entrypoint as a child process.

The resolver is deliberately narrow: it finds exactly one row, recomputes its digest, and turns it into a child-process location. It is not a database extension loader, a remote execution service, or a sandbox.

Write the registry

Package a worker with SQL

vgi_worker_package() is a convenience table macro over DuckDB's own read_blob(), pragma_platform(), and hashing primitives. It is not a separate importer: materialize or insert the one row it returns using normal SQL and normal transactions.

create-worker-registry.sql
LOAD vgi;

-- Keep the registry in an ordinary persistent DuckDB database.
ATTACH 'worker-registry.duckdb' AS worker_registry;

-- vgi_worker_package() uses read_blob(), detects the archive format,
-- fills in pragma_platform(), and computes the SHA-256.
CREATE TABLE worker_registry.main.worker_packages AS
SELECT * FROM vgi_worker_package(
  'dist/acme-worker.tar.gz',
  'acme-worker',
  '1.4.0',
  entrypoint := 'bin/acme-worker'
);

ALTER TABLE worker_registry.main.worker_packages
  ADD PRIMARY KEY (worker_name, platform, package_version);

CHECKPOINT;

The source file is needed only while that statement runs. After commit, the complete bytes live in contents; a reader does not need the build directory or the original artifact path.

add-worker-builds.sql
-- Add another immutable build using normal SQL.
INSERT INTO worker_registry.main.worker_packages
SELECT * FROM vgi_worker_package(
  'dist/acme-worker-1.5.0',
  'acme-worker',
  '1.5.0'
);

-- Cross-packaging: use the exact pragma_platform() value of the target.
INSERT INTO worker_registry.main.worker_packages
SELECT * FROM vgi_worker_package(
  'dist/acme-worker-linux-arm64.tar.gz',
  'acme-worker',
  '1.5.0',
  entrypoint := 'bin/acme-worker',
  platform := 'linux_arm64'
);

Seven required columns

Package table contract

The table name is yours. VGI only requires the columns below; the packaging macro also emits an optional created_at timestamp. Additional publisher metadata is fine.

Column Type Meaning
worker_name VARCHAR Stable package name used by the URI.
platform VARCHAR Exact consumer platform from pragma_platform().
package_version VARCHAR Publisher-selected immutable version.
package_format VARCHAR Executable or supported archive/compression format.
entrypoint VARCHAR Relative executable path after extraction.
contents BLOB Complete executable or archive bytes.
sha256 VARCHAR 64-character SHA-256 of contents.

VGI counts all matching rows and fails if the coordinate is ambiguous. A primary key on (worker_name, platform, package_version) makes that invariant explicit.

Read in a new process

Attach from the registry

The location has four path components — catalog, schema, table, and worker — followed by a required package version:

database://catalog/schema/table/worker?package_version=version[#sha256=digest]

Catalog, schema, and table are resolved as quoted identifiers. Worker name, platform, and version are bound values rather than SQL text. Percent-encode names containing /, ?, #, or %.

attach-database-worker.sql
-- The registry may be attached read-only.
ATTACH 'worker-registry.duckdb' AS worker_registry (READ_ONLY);

ATTACH 'acme' AS acme (
  TYPE vgi,
  LOCATION 'database://worker_registry/main/worker_packages/acme-worker?package_version=1.4.0'
);

SELECT * FROM acme.main.events LIMIT 10;

Resolution uses a dedicated database connection. Commit the package row before attaching; temporary tables and uncommitted changes are intentionally not visible. The attached registry itself can be read-only.

pin-the-package.sql
-- The row digest is always recomputed. For an out-of-band pin,
-- append the expected 64-character SHA-256 as a URI fragment.
ATTACH 'acme' AS pinned_acme (
  TYPE vgi,
  LOCATION 'database://worker_registry/main/worker_packages/acme-worker?package_version=1.4.0#sha256=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
);

What the BLOB contains

Formats and platforms

A worker is often more than one file. Store the whole runtime as an archive and name the executable inside it; VGI extracts the package with bounded, traversal-safe archive rules.

executable

A directly runnable file. raw and binary are accepted aliases.

zip · tar · tar.gz · tar.zst

Multi-file archives. Supply an explicit relative entrypoint.

gzip · zstd

A single compressed executable. The decompressed filename becomes the entrypoint unless overridden.

package_format := 'auto' recognizes those filename extensions. Everything else is treated as a raw executable. Archive packages require an explicit, relative entrypoint.

Store one row per supported platform. By default the macro records the packaging machine's exact pragma_platform() value; pass platform := … when inserting a cross-compiled build. At attach time there is no fallback to a “close enough” architecture — the platform must match exactly.

Reuse without races

Cache and cleanup

Cache identity includes the verified content digest, normalized format, and entrypoint. Installation happens through a private staging directory and atomic rename, so concurrent DuckDB processes converge on one immutable copy. Running catalogs and pooled subprocesses hold cross-process leases; cleanup skips an artifact while any process still uses it.

worker-cache.sql
-- See every immutable package materialized on this machine.
SELECT * FROM vgi_worker_cache();

-- Apply the configured age and size limits now.
SELECT * FROM vgi_worker_cache_prune();

-- Remove everything not protected by a running catalog or process lease.
DETACH acme;
SELECT * FROM vgi_worker_pool_flush();
SELECT * FROM vgi_worker_cache_flush();
Setting Default Limit
vgi_worker_cache_max_bytes 5 GiB Maximum managed cache size.
vgi_worker_cache_ttl_seconds 30 days Remove packages unused for this long.
vgi_worker_package_max_bytes 512 MiB Maximum stored/compressed BLOB size.
vgi_worker_package_max_extracted_bytes 1 GiB Maximum extracted package size.
vgi_worker_package_max_files 10,000 Maximum archive entries.

Set vgi_worker_cache_dir to choose the cache root. It must be on a filesystem that permits execution. With no override, VGI follows its release cache and then XDG_CACHE_HOME or the current user's cache directory.

Trust boundary

A verified package is still native code

Attaching a database package executes code supplied by that database. Use registries whose writers you trust. SHA-256 verification proves which bytes you received; it does not sandbox them or decide whether they are safe.

VGI recomputes the row digest before cache lookup and can require an independent digest pin in the URI. Cached entrypoints are executed directly rather than through a shell. Archive entries must be relative and normalized; traversal, links, backslashes, drive prefixes, excessive file counts, and oversized extraction are rejected before installation.

Because execution requires a native child process, database:// is for desktop and server DuckDB builds. It is not available inside browser DuckDB-WASM.

Before handing it over

Production checklist

01

Use a committed, non-temporary table. Resolution runs through a dedicated connection, so temporary tables and uncommitted rows are intentionally invisible.

02

Enforce one row per worker, platform, and package version with a primary key or unique constraint.

03

Treat package versions as immutable. Publish a new version instead of replacing bytes under an existing coordinate.

04

Build and test every platform you publish. A package row must contain native code for the machine that will execute it.

05

Use an out-of-band digest pin when the expected build is known independently of the registry.

06

Attach only registries whose writers you trust. Integrity verification detects changed bytes; it does not make untrusted native code safe.

Keep going