Skip to content

Commit 095d3b5

Browse files
rustyconoverclaude
andcommitted
feat(example): commit query_seed/overlapping/narrow_bind fixtures + wire worker
vgi main's integration suite now requires three worker-side fixtures that had only ever existed as uncommitted WIP, so CI (which tracks vgi main) was red on five tests across every transport: - query_seed scalar (scalar count 41 -> 42) - overlapping_range_partitioned table fn (table count 94 -> 95; partition_columns.test exercises OVERLAPPING_PARTITIONS) - narrow_bind catalog + scan handlers (narrow_bind_mismatch.test: bind-time column-count mismatch must fail closed, not segfault the client) Commit all three fixtures and register them via examples/all, and restore the narrow_bind catalog-handler composition + RegisterAll wiring in the example worker that 06e6ac8 had to drop because the package was uncommitted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 75d6f37 commit 095d3b5

5 files changed

Lines changed: 402 additions & 4 deletions

File tree

cmd/vgi-example-worker/main.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/Query-farm/vgi-go/examples/accumulate"
1616
"github.com/Query-farm/vgi-go/examples/all"
17+
"github.com/Query-farm/vgi-go/examples/narrow_bind"
1718
"github.com/Query-farm/vgi-go/examples/schema_reconcile"
1819
"github.com/Query-farm/vgi-go/examples/table"
1920
"github.com/Query-farm/vgi-go/internal/covflush"
@@ -96,7 +97,7 @@ func main() {
9697
}),
9798
// Cross-language reproducer catalogs share this binary; ATTACH
9899
// against any of these names succeeds (functions are catalog-agnostic).
99-
vgi.WithCatalogAliases("projection_repro", "schema_reconcile"),
100+
vgi.WithCatalogAliases("projection_repro", "schema_reconcile", narrow_bind.CatalogName),
100101
// The accumulate fixture catalog is discoverable (data version 2.0.0)
101102
// and isolated per ATTACH (random scope); its functions are registered
102103
// catalog-scoped below so they don't leak into the example catalog.
@@ -106,9 +107,27 @@ func main() {
106107
// served by handlers (not declared via RegisterCatalogTable). The four
107108
// handlers below are wired in registration order; they short-circuit on
108109
// any other catalog name.
109-
vgi.WithSchemaContentsHandler(schema_reconcile.SchemaContentsHandler),
110-
vgi.WithAttachTableGetHandler(schema_reconcile.AttachTableGetHandler),
111-
vgi.WithAttachScanFunctionGetHandler(schema_reconcile.AttachScanFunctionGetHandler),
110+
// Each option is single-valued, so the schema_reconcile and narrow_bind
111+
// fixtures are composed: try the first, fall through to the second when
112+
// it declines (returns false).
113+
vgi.WithSchemaContentsHandler(func(attach []byte, schema string) ([]vgi.SerializedSchemaItem, bool) {
114+
if items, ok := schema_reconcile.SchemaContentsHandler(attach, schema); ok {
115+
return items, true
116+
}
117+
return narrow_bind.SchemaContentsHandler(attach, schema)
118+
}),
119+
vgi.WithAttachTableGetHandler(func(attach []byte, schema, name string, atUnit, atValue *string) ([]byte, bool, error) {
120+
if data, ok, err := schema_reconcile.AttachTableGetHandler(attach, schema, name, atUnit, atValue); ok || err != nil {
121+
return data, ok, err
122+
}
123+
return narrow_bind.AttachTableGetHandler(attach, schema, name, atUnit, atValue)
124+
}),
125+
vgi.WithAttachScanFunctionGetHandler(func(attach []byte, schema, name string, atUnit, atValue *string) (*vgi.ScanFunctionResult, bool, error) {
126+
if res, ok, err := schema_reconcile.AttachScanFunctionGetHandler(attach, schema, name, atUnit, atValue); ok || err != nil {
127+
return res, ok, err
128+
}
129+
return narrow_bind.AttachScanFunctionGetHandler(attach, schema, name, atUnit, atValue)
130+
}),
112131
vgi.WithAttachScanBranchesGetHandler(multiBranchScanBranchesGet),
113132
vgi.WithAttachWriteFunctionGetHandler(schema_reconcile.AttachWriteFunctionGetHandler),
114133
vgi.WithSecretTypes(
@@ -177,6 +196,11 @@ func main() {
177196
// the example catalog and preserves its function inventory).
178197
accumulate.Register(w)
179198

199+
// Register the narrow_bind reproducer fixture (catalog-scoped). Its
200+
// scan functions back the mismatch/consistent tables advertised by the
201+
// composed catalog handlers above.
202+
narrow_bind.RegisterAll(w)
203+
180204
// Writable catalog (in-memory, per-process state). Gated off by default so
181205
// the example worker's function inventory matches the reference vgi-python
182206
// worker; set VGI_WORKER_ENABLE_WRITABLE=1 to exercise the writable tests.

examples/all/all.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func registerScalars(w *vgi.Worker) {
5757
w.RegisterScalar(&scalar.PairTypeIntIntFunction{})
5858
w.RegisterScalar(&scalar.PairTypeStrStrFunction{})
5959
w.RegisterScalar(&scalar.PairTypeIntStrFunction{})
60+
w.RegisterScalar(scalar.NewQuerySeed())
6061
w.RegisterScalar(&scalar.RandomBytesFunction{})
6162
w.RegisterScalar(&scalar.RandomIntFunction{})
6263
w.RegisterScalar(&scalar.ReturnSecretValueFunction{})
@@ -108,6 +109,7 @@ func registerTables(w *vgi.Worker) {
108109
w.RegisterTable(table.NewRegionYearPartitionedFunction())
109110
w.RegisterTable(table.NewPartitionedWithExplicitOverrideFunction())
110111
w.RegisterTable(table.NewDisjointRangePartitionedFunction())
112+
w.RegisterTable(table.NewOverlappingRangePartitionedFunction())
111113
w.RegisterTable(table.NewBrokenMissingPartitionValuesFunction())
112114
w.RegisterTable(table.NewBrokenPartitionMinNeqMaxFunction())
113115
w.RegisterTable(table.NewBrokenPartitionValuesNoAnnotationFunction())

examples/narrow_bind/handler.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
2+
3+
// Package narrow_bind is the cross-language reproducer for a VGI worker whose
4+
// bind output_schema disagrees with the table's advertised columns. Two
5+
// virtual tables live in the narrow_bind catalog:
6+
//
7+
// - mismatch — advertises columns {id, val} in its catalog listing but its
8+
// scan function narrow_scan binds to {id} only. This is the inconsistency
9+
// that used to walk off the end of the worker's 1-column batch in
10+
// ArrowTableFunction::ArrowToDuckDB (a hard client SIGSEGV). The client
11+
// must now refuse it at bind with a clear BinderException.
12+
//
13+
// - consistent — advertises {id, val} and its scan function wide_scan binds
14+
// to {id, val}. Positive control: this must keep working unchanged.
15+
//
16+
// Backs test/sql/integration/narrow_bind_mismatch.test in the vgi extension.
17+
package narrow_bind
18+
19+
import (
20+
"context"
21+
22+
"github.com/Query-farm/vgi-go/vgi"
23+
"github.com/Query-farm/vgi-rpc-go/vgirpc"
24+
"github.com/apache/arrow-go/v18/arrow"
25+
"github.com/apache/arrow-go/v18/arrow/array"
26+
"github.com/apache/arrow-go/v18/arrow/memory"
27+
)
28+
29+
// CatalogName is the SQL catalog name this fixture publishes.
30+
const CatalogName = "narrow_bind"
31+
32+
// SchemaName is the only schema this fixture publishes.
33+
const SchemaName = "main"
34+
35+
// tableSchema is what the catalog advertises for both tables: two columns.
36+
var tableSchema = arrow.NewSchema([]arrow.Field{
37+
{Name: "id", Type: arrow.PrimitiveTypes.Int64, Nullable: true},
38+
{Name: "val", Type: arrow.PrimitiveTypes.Int64, Nullable: true},
39+
}, nil)
40+
41+
// narrowBindSchema is what the narrow scan function actually binds to: one
42+
// column. The deliberate disagreement with tableSchema is the bug under test.
43+
var narrowBindSchema = arrow.NewSchema([]arrow.Field{
44+
{Name: "id", Type: arrow.PrimitiveTypes.Int64, Nullable: true},
45+
}, nil)
46+
47+
// tableFunctions maps each advertised table to the scan function that backs it.
48+
// Both tables advertise tableSchema (2 cols); narrow_scan binds to 1 col.
49+
var tableFunctions = map[string]string{
50+
"mismatch": "narrow_scan",
51+
"consistent": "wide_scan",
52+
}
53+
54+
// scanState tracks whether the single output batch has been emitted.
55+
type scanState struct {
56+
Emitted bool
57+
}
58+
59+
// ============================================================================
60+
// narrow_scan — binds to a NARROWER schema than the catalog advertises (bug).
61+
// ============================================================================
62+
63+
type narrowScan struct{}
64+
65+
var _ vgi.TypedTableFunc[scanState] = (*narrowScan)(nil)
66+
67+
func (narrowScan) Name() string { return "narrow_scan" }
68+
func (narrowScan) Metadata() vgi.FunctionMetadata {
69+
return vgi.FunctionMetadata{
70+
Description: "bind reports a narrower schema than the table advertises",
71+
Stability: vgi.StabilityConsistent,
72+
}
73+
}
74+
func (narrowScan) ArgumentSpecs() []vgi.ArgSpec {
75+
return []vgi.ArgSpec{
76+
{Name: "count", Position: 0, ArrowType: "int64", Doc: "rows", IsConst: true},
77+
}
78+
}
79+
func (narrowScan) OnBind(_ *vgi.BindParams) (*vgi.BindResponse, error) {
80+
return &vgi.BindResponse{OutputSchema: narrowBindSchema}, nil
81+
}
82+
func (narrowScan) Cardinality(_ *vgi.BindParams) (*vgi.TableCardinality, error) {
83+
return &vgi.TableCardinality{Estimate: -1, Max: -1}, nil
84+
}
85+
func (narrowScan) OnInit(_ *vgi.InitParams) (*vgi.GlobalInitResponse, error) {
86+
return &vgi.GlobalInitResponse{MaxWorkers: 1}, nil
87+
}
88+
func (narrowScan) NewState(_ *vgi.ProcessParams) (*scanState, error) { return &scanState{}, nil }
89+
func (narrowScan) Process(_ context.Context, params *vgi.ProcessParams, state *scanState, out *vgirpc.OutputCollector) error {
90+
// In practice the client refuses this scan at bind (1 col vs 2 advertised),
91+
// so Process is never reached. Emit a single {id} batch defensively.
92+
if state.Emitted {
93+
return out.Finish()
94+
}
95+
state.Emitted = true
96+
// Emit takes ownership of the batch reference; do not release it here.
97+
if err := out.Emit(buildBatch(params.OutputSchema, []int64{0, 1, 2}, nil)); err != nil {
98+
return err
99+
}
100+
return out.Finish()
101+
}
102+
103+
// ============================================================================
104+
// wide_scan — binds to the full advertised schema (positive control).
105+
// ============================================================================
106+
107+
type wideScan struct{}
108+
109+
var _ vgi.TypedTableFunc[scanState] = (*wideScan)(nil)
110+
111+
func (wideScan) Name() string { return "wide_scan" }
112+
func (wideScan) Metadata() vgi.FunctionMetadata {
113+
return vgi.FunctionMetadata{
114+
Description: "bind matches the table's advertised schema",
115+
Stability: vgi.StabilityConsistent,
116+
}
117+
}
118+
func (wideScan) ArgumentSpecs() []vgi.ArgSpec {
119+
return []vgi.ArgSpec{
120+
{Name: "count", Position: 0, ArrowType: "int64", Doc: "rows", IsConst: true},
121+
}
122+
}
123+
func (wideScan) OnBind(_ *vgi.BindParams) (*vgi.BindResponse, error) {
124+
return &vgi.BindResponse{OutputSchema: tableSchema}, nil
125+
}
126+
func (wideScan) Cardinality(_ *vgi.BindParams) (*vgi.TableCardinality, error) {
127+
return &vgi.TableCardinality{Estimate: -1, Max: -1}, nil
128+
}
129+
func (wideScan) OnInit(_ *vgi.InitParams) (*vgi.GlobalInitResponse, error) {
130+
return &vgi.GlobalInitResponse{MaxWorkers: 1}, nil
131+
}
132+
func (wideScan) NewState(_ *vgi.ProcessParams) (*scanState, error) { return &scanState{}, nil }
133+
func (wideScan) Process(_ context.Context, params *vgi.ProcessParams, state *scanState, out *vgirpc.OutputCollector) error {
134+
if state.Emitted {
135+
return out.Finish()
136+
}
137+
state.Emitted = true
138+
// Emit takes ownership of the batch reference; do not release it here.
139+
if err := out.Emit(buildBatch(params.OutputSchema, []int64{0, 1, 2}, []int64{10, 20, 30})); err != nil {
140+
return err
141+
}
142+
return out.Finish()
143+
}
144+
145+
// buildBatch assembles a batch matching schema. It carries an "id" column
146+
// always and a "val" column when vals is non-nil, projecting to whatever
147+
// columns the (possibly pruned) output schema requests.
148+
func buildBatch(schema *arrow.Schema, ids, vals []int64) arrow.RecordBatch {
149+
mem := memory.NewGoAllocator()
150+
cols := make([]arrow.Array, schema.NumFields())
151+
for i := 0; i < schema.NumFields(); i++ {
152+
b := array.NewInt64Builder(mem)
153+
switch schema.Field(i).Name {
154+
case "id":
155+
b.AppendValues(ids, nil)
156+
case "val":
157+
if vals != nil {
158+
b.AppendValues(vals, nil)
159+
} else {
160+
for range ids {
161+
b.AppendNull()
162+
}
163+
}
164+
default:
165+
for range ids {
166+
b.AppendNull()
167+
}
168+
}
169+
cols[i] = b.NewArray()
170+
b.Release()
171+
}
172+
batch := array.NewRecordBatch(schema, cols, int64(len(ids)))
173+
for _, c := range cols {
174+
c.Release()
175+
}
176+
return batch
177+
}
178+
179+
// ============================================================================
180+
// Catalog wiring — the narrow_bind catalog advertises mismatch/consistent and
181+
// routes each table's SELECT to its scan function via the attach handlers.
182+
// ============================================================================
183+
184+
func serializeTableInfo(name string) ([]byte, error) {
185+
info := &vgi.TableInfo{
186+
Name: name,
187+
SchemaName: SchemaName,
188+
Comment: "narrow-bind reproducer table -> " + tableFunctions[name],
189+
Columns: tableSchema,
190+
}
191+
return vgi.SerializeTableInfo(info)
192+
}
193+
194+
// SchemaContentsHandler returns the table list for the narrow_bind catalog.
195+
// Wired via vgi.WithSchemaContentsHandler (composed with other fixtures).
196+
func SchemaContentsHandler(attachOpaqueData []byte, schemaName string) ([]vgi.SerializedSchemaItem, bool) {
197+
if string(attachOpaqueData) != CatalogName || schemaName != SchemaName {
198+
return nil, false
199+
}
200+
items := make([]vgi.SerializedSchemaItem, 0, len(tableFunctions))
201+
for _, name := range []string{"consistent", "mismatch"} {
202+
data, err := serializeTableInfo(name)
203+
if err != nil {
204+
continue
205+
}
206+
items = append(items, data)
207+
}
208+
return items, true
209+
}
210+
211+
// AttachTableGetHandler answers single-table catalog_table_get RPCs for the
212+
// narrow_bind catalog. Wired via vgi.WithAttachTableGetHandler.
213+
func AttachTableGetHandler(attachOpaqueData []byte, schemaName, name string, _, _ *string) ([]byte, bool, error) {
214+
if string(attachOpaqueData) != CatalogName || schemaName != SchemaName {
215+
return nil, false, nil
216+
}
217+
if _, ok := tableFunctions[name]; !ok {
218+
return nil, false, nil
219+
}
220+
data, err := serializeTableInfo(name)
221+
if err != nil {
222+
return nil, true, err
223+
}
224+
return data, true, nil
225+
}
226+
227+
// AttachScanFunctionGetHandler routes SELECT-time scan-function lookups to the
228+
// table's backing scan function (mismatch->narrow_scan, consistent->wide_scan).
229+
// Wired via vgi.WithAttachScanFunctionGetHandler.
230+
func AttachScanFunctionGetHandler(attachOpaqueData []byte, schemaName, name string, _, _ *string) (*vgi.ScanFunctionResult, bool, error) {
231+
if string(attachOpaqueData) != CatalogName || schemaName != SchemaName {
232+
return nil, false, nil
233+
}
234+
fn, ok := tableFunctions[name]
235+
if !ok {
236+
return nil, false, nil
237+
}
238+
return &vgi.ScanFunctionResult{
239+
FunctionName: fn,
240+
PositionalArguments: []vgi.ScanArg{
241+
{Value: int64(3), Type: arrow.PrimitiveTypes.Int64},
242+
},
243+
}, true, nil
244+
}
245+
246+
// RegisterAll registers the two scan functions on the worker, scoped to the
247+
// narrow_bind catalog so they don't clutter the example catalog's listing.
248+
func RegisterAll(w *vgi.Worker) {
249+
w.RegisterTableForCatalog(CatalogName, vgi.AsTableFunction[scanState](narrowScan{}))
250+
w.RegisterTableForCatalog(CatalogName, vgi.AsTableFunction[scanState](wideScan{}))
251+
}

examples/scalar/query_seed.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
2+
3+
package scalar
4+
5+
import (
6+
"context"
7+
8+
"github.com/Query-farm/vgi-go/vgi"
9+
"github.com/apache/arrow-go/v18/arrow"
10+
"github.com/apache/arrow-go/v18/arrow/array"
11+
)
12+
13+
// QuerySeedFunction adds a per-query-stable seed to each input value.
14+
//
15+
// It is the only example fixture that emits
16+
// vgi.StabilityConsistentWithinQuery, so the wire path for the third
17+
// stability variant stays exercised. Semantically the seed is fixed for the
18+
// duration of a single query but may differ across queries (like now()); the
19+
// offset is a constant here so SQL tests have a stable expected output — the
20+
// stability flag is what is under test, not the numeric result.
21+
type QuerySeedFunction struct{}
22+
23+
type querySeedArgs struct {
24+
Value *array.Int64 `vgi:"pos=0,const=false,doc=Value to offset"`
25+
}
26+
27+
func (*QuerySeedFunction) Name() string { return "query_seed" }
28+
29+
func (*QuerySeedFunction) Metadata() vgi.FunctionMetadata {
30+
return vgi.FunctionMetadata{
31+
Description: "Add a per-query-stable seed to each value (demonstrates CONSISTENT_WITHIN_QUERY stability)",
32+
Stability: vgi.StabilityConsistentWithinQuery,
33+
ReturnType: arrow.PrimitiveTypes.Int64,
34+
}
35+
}
36+
37+
func (*QuerySeedFunction) OnBindTyped(_ *querySeedArgs, _ *vgi.BindParams) (*vgi.BindResponse, error) {
38+
return vgi.BindResult(arrow.PrimitiveTypes.Int64)
39+
}
40+
41+
func (*QuerySeedFunction) ProcessTyped(_ context.Context, args *querySeedArgs, params *vgi.ProcessParams, batch arrow.RecordBatch) (arrow.RecordBatch, error) {
42+
return vgi.GenerateColumn(params, batch, array.NewInt64Builder,
43+
func(i int) int64 {
44+
return args.Value.Value(i) + 1000
45+
})
46+
}
47+
48+
// NewQuerySeed returns the registration-ready ScalarFunction.
49+
func NewQuerySeed() vgi.ScalarFunction {
50+
return vgi.AsScalarFunction[querySeedArgs](&QuerySeedFunction{})
51+
}

0 commit comments

Comments
 (0)