Skip to content
Query.Farm
Talk with Us

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.

A malformed tag panics at startup

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.

Keys are case-insensitive and comma-separated. Put doc= last, since its value may itself contain commas.

KeyMeaning
pos=NPositional index. Default -1, meaning named-only.
name=XArgument name. Defaults to the snake_case of the field name.
constIsConst, and it defaults to true. Write const=false for a column argument — the one default most likely to surprise you.
default=VGives 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=XOverride the inferred Arrow type. See the warning below.
varargsA slice field consumes Positional[pos:]. The spec advertises the element type, not the slice.
bound=a+bType-bound predicates, OR-ed with +. Pair with an any field.
-Skip this field entirely.

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.

KeyMeaning
ge=, le=, gt=, lt=Numeric bounds, inclusive and exclusive.
choices=a,b,cA 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.

Inference means you rarely need type= at all.

Go field typeArrow type
stringvarchar
int, int64int64
int32, int16, int8matching int width
uint64, uint32, …matching uint width
float64double
float32float
boolbool
[]byteblob
[]Tlist of inferred T
[N]T (N>0, T≠byte)fixed-size list of N inferred T
struct{…}struct, field names preserved
time.Timetimestamp[us, UTC]
interface{} / anyany — pair with bound=
`type=` takes Arrow names, not SQL names

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.

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.

A bound argument arrives untyped

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.

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 isA literal, fixed for the whole scanA column, one value per row
Available atBind — read it in OnBind / NewStateProcess — read it off the batch
ConstraintsEnforced at bindAdvisory; describe the argument for discovery
Bound into your struct?Yes, by BindArgsNo — 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.

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"},
  }
}