extend VolatileArgs opt-in to vtab Filter and Updater Insert/Update
Follow-up to !120 (merged) (#226).
Background
!120 (merged) added FunctionImpl.VolatileArgs as a strict opt-in for zero-copy TEXT/BLOB access on scalar and aggregate UDF callbacks but explicitly left vtab Filter and Update on the non-volatile path. From the !120 (merged) description:
Vtab
FilterandUpdateare deliberately out of scope here — those trampolines passfalseexplicitly. A future MR can extend the opt-in to vtab if there's demand.
This MR is that follow-up.
What this MR does
Extends the same opt-in to:
Cursor.Filter(xFilter)Updater.Insert/Updater.Update(xUpdate)
The contract on those callbacks already says "the vals/cols slice and its entries are not valid past the return of this method; implementations must copy any value they wish to retain", so the API surface is unchanged for users who do not opt in; only the body-allocation strategy differs when opt-in is set.
Opt-in is exposed as a new optional interface in package vtab:
type VolatileArgsOpter interface {
VolatileArgs() bool
}A Module that implements it and returns true gets zero-copy TEXT/BLOB views for every Filter, Insert, and Update call routed to tables created from that module. The bool is read once at registration into goModule and shared by every table, so the hot path is one extra field read per trampoline, not a type assertion.
When the interface is not implemented (the default for all existing modules), behavior is byte-for-byte identical to current master.
Safety contract
The docstring on VolatileArgsOpter cross-references FunctionImpl.VolatileArgs and restates the same rules so vtab authors don't have to chase the link:
- Retention rule: the
stringand[]bytevalues insidevals/colsare valid only for the duration of the call. Must not be retained past return, directly or via anything that captures them. - Failure mode: deterministic corruption invisible to
-race(vtab callbacks run sequentially on one goroutine, same as UDF). - Safe-copy idioms for callbacks that must keep values across rows:
saved := append([]byte(nil), v.([]byte)...) saved := strings.Clone(v.(string)) - Within-callback re-entrancy hazard carried over from !120 (merged) patchset 2: nested
Query/Execon the same connection during a volatile-args callback can cause SQLite to reuse the underlying value buffers, so a volatilestring/[]byteread before the nested call may alias different bytes after it returns. - No effect on integer, float, time, or NULL arguments.
- "When in doubt, leave it off" guidance: the non-volatile path is already cheap after !114 (merged).
Benchmark
darwin/arm64 (Apple M3), vtab with one TEXT and one BLOB argument per call, -benchtime=2s:
BenchmarkVTabFilterArgs-8 1014 ns/op 616 B/op 20 allocs/op
BenchmarkVTabFilterArgsVolatile-8 982 ns/op 608 B/op 18 allocs/op
BenchmarkVTabUpdateArgs-8 712 ns/op 256 B/op 13 allocs/op
BenchmarkVTabUpdateArgsVolatile-8 686 ns/op 248 B/op 11 allocs/opTwo allocations and eight bytes saved per call: one libc.GoString for the TEXT arg, one make([]byte) for the BLOB. Same shape as the UDF case in !120 (merged) (2000 fewer allocs over 1000 rows of 1 TEXT + 1 BLOB).
The remaining allocs are upstream of the trampoline (cursor open/close, statement prep, BestIndex, result handling) and out of scope here.
Tests
TestVTabVolatileFilter— registers aModulewithVolatileArgs() boolreturningtrue, queries a virtual table with TEXT + BLOB constraints over three cases (non-empty, non-empty, empty). The recorder usesstrings.CloneandcloneBytesto demonstrate the required safe-copy pattern.TestVTabVolatileUpdate— same shape for thexUpdatepath. Uses a small in-memoryUpdatertable so bothInsert(no old rowid) andUpdate(matched-by-rowid) trampolines fire with correct TEXT/BLOB column values.- Full
go test -count=1 ./...stays green; pre-existing vtab tests (TestDummyModuleVtab,TestVtabConstraintArgIndex,TestVtabUpdaterInsertUpdateDelete, etc.) untouched.
Credit
API shape and "explicit opt-in, footgun behind a boolean" framing follow @cznic's design from !114 (merged) / !120 (merged) directly; this MR just extends the same surface to vtab as the !120 (merged) description invited.