Skip to content

Errors & logging

How failures reach the user, and the three logging channels you have.

Errors become SQL errors

When your function throws, the framework propagates the exception to the engine and it surfaces as a SQL error carrying your message. So the message is the user experience — make it specific and function-named:

java
try {
    s.total = Math.addExact(s.total, b.get(i));
} catch (ArithmeticException e) {
    throw new IllegalArgumentException("vgi_sum: int64 overflow", e);
}
Error: vgi_sum: int64 overflow

Fail at bind time, not mid-stream

Validate arguments in onBind() (or argumentSpecs/outputType) so bad calls are rejected before any rows are produced — a cleaner error and no wasted work. ParameterExtractor gives you typed, well-worded validation for free:

java
@Override public BindResponse onBind(TableBindParams params) {
    ParameterExtractor p = ParameterExtractor.of(params.arguments());
    p.positional(0, "count").asLong().notNull();      // bind-time error if missing/null
    p.named("batch_size").asLong().ge(1).notNull();
    return super.onBind(params);
}

For scalars, a typeBound on a @Vector(any = true) argument produces a SQL-typed bind error automatically, e.g. double: value must be numeric (got VARCHAR). See scalar.

A useful split: bind errors are for "this call can never work" (wrong types, missing args); execution errors are for "this data can't be processed" (overflow, a parse failure on row N).

Three logging channels

ChannelGoes toUse for
SLF4J (your logger)the worker's stderrworker-side diagnostics, request traces
clientLogthe engine's duckdb_logs()messages the user should see for their query
Debug env varsthe worker's stderrtransport-level tracing (shm)

1. Worker-side logging (SLF4J)

VGI uses SLF4J; add any binding (the examples use slf4j-simple). Log as normal:

java
private static final Logger log = LoggerFactory.getLogger(MyFunction.class);
// …
log.info("scanning {} rows for {}", count, ctx.requestId());

Under the launch: transport the launcher redirects the worker's fd 2 to /dev/null, so set VGI_WORKER_STDERR to capture it:

bash
VGI_WORKER_STDERR=/tmp/worker.log   # in the worker's environment

This is the first place to look when a launch: worker misbehaves.

2. Client-visible logging (clientLog)

To surface a message to the user — it shows up in the engine's duckdb_logs() — use the call context (or the output collector in a producer):

java
// in a process/combine/exchange method that has a CallContext `ctx`:
ctx.clientLog(Level.INFO, "buffered " + n + " batches");

// in a table producer's produceTick(out, ctx):
out.clientLog(new Message(Level.INFO, "generation complete", null));
sql
SELECT * FROM demo.my_fn(...);
SELECT message FROM duckdb_logs() WHERE message LIKE '%buffered%';

Use this sparingly — for progress or diagnostics the query author would actually want, not for worker-internal noise (that's SLF4J's job).

3. Transport debug

VGI_RPC_SHM_DEBUG=1 logs shared-memory attach/resolve/fallback and a per-connection CPU timeline to the worker's stderr — handy when checking whether shm is engaging. See shared memory.

Debugging a launch: worker

launch: workers are long-lived and pooled, which has two implications:

  • To see a crash: set VGI_WORKER_STDERR=/tmp/w.log; without it, fd 2 is /dev/null and the stack trace is lost.
  • After a rebuild: the pool keeps serving the old binary until it exits. pkill -f <your main class> (or wait out --idle-timeout) before re-attaching, or new functions appear missing and old ones run stale code.

See CLI & environment for the full env-var list.