Expose a catalog
Functions are one half of what a worker can serve; the other is the catalog — the schemas,
tables, views and macros SQL users expect. None of these extend a base class: you build small
descriptor objects, register them on the Worker builder, and DuckDB surfaces them like any local
catalog object.
The whole example
Section titled “The whole example”CatalogExample.java
// VGI-Java example: constructing a catalog (schema · table · view · macros).
//
// Functions are one half of what a worker can serve; the other is the catalog —
// the schemas, tables, views, and macros SQL users expect. None of these extend
// a base class. You build small descriptor objects and register them on the
// Worker builder, and DuckDB surfaces them like any local catalog object.
//
// This builds a `catalog` schema with:
// - a table `first_five`, whose rows come from the `numbers` table function
// - a view `evens` over that table
// - a scalar macro `triple` and a table macro `below`
//
// ATTACH 'demo' AS demo (TYPE vgi, LOCATION 'launch:/abs/path/bin/runCatalog');
// SELECT * FROM demo.catalog.first_five; -- 0,1,2,3,4
// SELECT * FROM demo.catalog.evens; -- 0,2,4
package farm.query.vgi.examples;
import farm.query.vgi.Worker;
import farm.query.vgi.catalog.CatalogTable;
import farm.query.vgi.catalog.Macro;
import farm.query.vgi.catalog.MacroType;
import farm.query.vgi.catalog.View;
import farm.query.vgi.internal.SchemaUtil;
import farm.query.vgi.types.Schemas;
import org.apache.arrow.vector.types.pojo.Schema;
import java.util.List;
import java.util.Map;
public final class CatalogExample {
// A table's columns are an Arrow schema, serialized to IPC bytes.
private static final byte[] FIRST_FIVE_COLUMNS = SchemaUtil.serializeSchema(
new Schema(List.of(Schemas.nullable("n", Schemas.INT64))));
/**
* Register the catalog objects onto an existing worker. The worker must also
* register the {@code numbers} table function ({@link TableExample}) that
* backs the table.
*/
public static Worker register(Worker w) {
return w
.schemaComment("catalog", "Tables, views, and macros built by hand")
// A table whose rows are produced by a function. `scanFunction`
// names the backing function and its arguments — here `numbers(5)`,
// which the worker itself serves. Its one INT64 output column maps
// (by position) onto the table's declared `n` column.
.registerCatalogTable(CatalogTable.builder("catalog", "first_five", FIRST_FIVE_COLUMNS)
.comment("The integers 0..4")
.scanFunction("numbers", List.of((Object) 5L), Map.of())
.cardinality(5, 5)
.build())
// A view is SQL the engine expands at query time.
.registerView(new View("catalog", "evens",
"SELECT n FROM catalog.first_five WHERE n % 2 = 0",
"Even values from first_five"))
// Macros are parameterized SQL the engine inlines at the call site.
.registerMacro(new Macro("catalog", "triple", MacroType.SCALAR,
List.of("x"), "x * 3", "Triple a value"))
.registerMacro(new Macro("catalog", "series", MacroType.TABLE,
List.of("k"), "SELECT n FROM range(k) t(n)",
"The integers 0..k-1"));
}
public static void main(String[] args) {
Worker w = Worker.builder()
.catalogName("demo")
.registerTable(new TableExample()); // the `numbers` function the table is backed by
register(w);
w.runFromArgs(args);
}
}
It builds a catalog schema holding a function-backed table, a view over it, and two macros.
Function-backed tables
Section titled “Function-backed tables”The table is not stored — its rows come from a table function the worker already registered:
SELECT * FROM demo.catalog.first_five;
Output
| n |
|---|
| 0 |
| 1 |
| 2 |
| 3 |
| 4 |
The user writes a table name and passes nothing. The numbers function and its baked-in arguments
are invisible to them, which is the point: a catalog is a way to publish a dataset, not an API.
VGI tables live in DuckDB’s catalog model, so you query them with ordinary qualified names, and
duckdb_tables() and duckdb_columns() see them. The difference is where the rows come from: a
native table is stored and managed by DuckDB, while a VGI table delegates its scan to your worker.
A view is pure SQL, evaluated by DuckDB once the definition has been advertised at attach time — there is no worker round trip for the view itself, only for whatever tables it reads:
SELECT * FROM demo.catalog.evens;
Output
| n |
|---|
| 0 |
| 2 |
| 4 |
Macros
Section titled “Macros”Java is the SDK that surfaces macros most directly — both scalar and table macros are catalog objects you register alongside tables and views. A macro is SQL text DuckDB expands at the call site, so it costs the worker nothing at query time; it is the right tool for a shorthand your users keep writing by hand.
A view or macro body that names demo.catalog.first_five is not fragile, even though it hardcodes
the catalog. The name in ATTACH must match the worker’s catalogName(...) — attaching under any
other name fails outright — so the name you declare is the name the definition is always read under.
What else a table can declare
Section titled “What else a table can declare”CatalogTable carries more than the example uses: inline schemas, comments and tags, NOT NULL and
other constraints, foreign keys, and per-column statistics that feed the optimizer. Beyond that,
the Java catalog supports semver data/implementation version negotiation, release manifests,
multi-branch tables, transactions and attach options — the richest catalog surface in the family.
Those are documented per type in the Catalogs reference; this page covers the shapes you need first.
Next steps
Section titled “Next steps”- The functions behind the tables → Function patterns.
- Serving it → Running a worker.
- Exact types → Catalogs.