Argument tags
A vgi:"..." tag is how a Go worker declares a function’s signature. One declaration drives two
things that would otherwise drift apart: the ArgSpec list the catalog advertises to DuckDB, and the
binding of runtime values back into your struct.
type geoArgs struct {
Latitude float64 `vgi:"pos=0,const=false,doc=Latitude column"`
Longitude float64 `vgi:"pos=1,const=false,doc=Longitude column"`
Precision int64 `vgi:"name=precision,default=4,ge=0,le=10,doc=Rounding precision"`
}
vgi.DeriveArgSpecs(geoArgs{}) turns that into specs; vgi.BindArgs(params.Args, &args) fills the
struct at runtime. The Typed* interfaces do both for you.
DeriveArgSpecs panics rather than returning an error — a bad tag is a programming mistake, and
failing at registration beats failing at query time. Expect to see it the first time you run the
worker, not on first use of the function.
Tag keys
Section titled “Tag keys”Keys are case-insensitive and comma-separated. Put doc= last, since its value may itself
contain commas.
| Key | Meaning |
|---|---|
pos=N | Positional index. Default -1, meaning named-only. |
name=X | Argument name. Defaults to the snake_case of the field name. |
const | IsConst, and it defaults to true. Write const=false for a column argument — the one default most likely to surprise you. |
default=V | Gives the argument a default; V is its string form, parsed at bind against the declared type. |
doc=… | Description, surfaced by vgi_function_arguments(). Single-quote it if it contains commas. |
type=X | Override the inferred Arrow type. See the warning below. |
varargs | A slice field consumes Positional[pos:]. The spec advertises the element type, not the slice. |
bound=a+b | Type-bound predicates, OR-ed with +. Pair with an any field. |
- | Skip this field entirely. |
Constraints
Section titled “Constraints”These are encoded into the argument’s Arrow field metadata, so vgi_function_arguments() and any
agent introspecting the catalog can describe the argument — and on a const argument they are
enforced at bind.
| Key | Meaning |
|---|---|
ge=, le=, gt=, lt= | Numeric bounds, inclusive and exclusive. |
choices=a,b,c | A closed set. Quote it (choices=‘a,b,c’) to keep the commas. Values are parsed against the declared type. |
pattern= | A regex the value must match. |
A violated constraint on a const argument produces a bind error naming the argument, its position and
the rule — the series(-1) failure in the table tutorial
is exactly this.
Go type → Arrow type
Section titled “Go type → Arrow type”Inference means you rarely need type= at all.
| Go field type | Arrow type |
|---|---|
string | varchar |
int, int64 | int64 |
int32, int16, int8 | matching int width |
uint64, uint32, … | matching uint width |
float64 | double |
float32 | float |
bool | bool |
[]byte | blob |
[]T | list of inferred T |
[N]T (N>0, T≠byte) | fixed-size list of N inferred T |
struct{…} | struct, field names preserved |
time.Time | timestamp[us, UTC] |
interface{} / any | any — pair with bound= |
The tag wants int64, not bigint; timestamp_us, not timestamp. Several SQL spellings are
valid — varchar, double, boolean, blob — which is exactly what makes the others look
plausible.
Since v0.22.0 an unrecognised name is rejected at registration with the right name suggested. On
v0.21.0 and earlier it silently became VARCHAR, and you found out much later from a bind error
naming the call site. Prefer omitting type= on either version.
Accepting several types with bound=
Section titled “Accepting several types with bound=”An any field accepts whatever DuckDB passes. bound= narrows that to a family, so overload
resolution can pick your function and the error is good when it can’t:
type doubleArgs struct {
Value any `vgi:"pos=0,const=false,bound=multipliable,doc=Numeric value to double"`
}
Available predicates: numeric, integer, floating, decimal, temporal, addable,
multipliable. Join with + for OR — bound=integer+floating.
The column comes through as an arrow.Array you must switch on. vgi.NumericDispatch handles the
common int64/float64 split for you; vgi.AsTyped[T] asserts a concrete array type.
Const vs column arguments
Section titled “Const vs column arguments”The distinction decides when your function sees the value, and const defaulting to true is the
single most common tag mistake.
const (default) | const=false | |
|---|---|---|
| Value is | A literal, fixed for the whole scan | A column, one value per row |
| Available at | Bind — read it in OnBind / NewState | Process — read it off the batch |
| Constraints | Enforced at bind | Advisory; describe the argument for discovery |
| Bound into your struct? | Yes, by BindArgs | No — the column is on the RecordBatch |
Leave const at its default for options like a precision or a mode; write const=false for the data
the function actually transforms.
What tags cannot declare
Section titled “What tags cannot declare”A TABLE argument — a whole relation streamed in — is not a value, so it cannot be bound into a
field. Write it as an explicit ArgSpec instead:
func (*FilterPositiveFn) ArgumentSpecs() []vgi.ArgSpec {
return []vgi.ArgSpec{
{Name: "data", Position: 0, ArrowType: "table", Doc: "Rows to filter"},
}
}
Next steps
Section titled “Next steps”- See them in use → Function patterns.
- The wire format they produce → Argument serialization.
- Exact types → Arguments.