@query-farm/vgi 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/arrow/impl-arrowjs/index.d.ts +1 -0
- package/dist/arrow/impl-arrowjs/metadata.d.ts +12 -0
- package/dist/arrow/impl-flechette/index.d.ts +1 -0
- package/dist/arrow/impl-flechette/metadata.d.ts +13 -0
- package/dist/arrow/index.d.ts +1 -1
- package/dist/client-entry.js +15 -4
- package/dist/client-entry.js.map +9 -8
- package/dist/functions/scalar.d.ts +10 -0
- package/dist/functions/table-in-out.d.ts +121 -0
- package/dist/functions/types.d.ts +12 -0
- package/dist/generated/vgi-client.d.ts +2 -0
- package/dist/index.core.d.ts +1 -1
- package/dist/index.js +295 -11
- package/dist/index.js.map +16 -15
- package/dist/protocol/types.d.ts +12 -0
- package/dist/serve-entry.js +13 -3
- package/dist/serve-entry.js.map +8 -7
- package/dist/worker-cf-entry.js +173 -16
- package/dist/worker-cf-entry.js.map +14 -13
- package/dist/worker.d.ts +1 -1
- package/package.json +4 -3
- package/src/arrow/impl-arrowjs/index.ts +1 -0
- package/src/arrow/impl-arrowjs/metadata.ts +20 -0
- package/src/arrow/impl-flechette/index.ts +1 -0
- package/src/arrow/impl-flechette/metadata.ts +25 -0
- package/src/arrow/index.ts +2 -0
- package/src/catalog/read-only.ts +9 -0
- package/src/client/client.ts +1 -0
- package/src/functions/registry.ts +53 -5
- package/src/functions/scalar.ts +17 -1
- package/src/functions/table-in-out.ts +480 -4
- package/src/functions/types.ts +12 -0
- package/src/generated/vgi-client.ts +3 -1
- package/src/generated/vgi-protocol-schemas.ts +2 -1
- package/src/index.core.ts +11 -1
- package/src/protocol/serializers/init.ts +7 -0
- package/src/protocol/types.ts +12 -0
- package/src/worker.ts +1 -1
package/dist/worker.d.ts
CHANGED
|
@@ -32,5 +32,5 @@ export declare class Worker {
|
|
|
32
32
|
* @param writable outgoing response sink (a `net.Socket`/`Duplex`, or an fd
|
|
33
33
|
* number). Omit for the stdout fd.
|
|
34
34
|
*/
|
|
35
|
-
serveStream(readable: ReadableStream<Uint8Array> | NodeJS.ReadableStream, writable?: number | import("node:net").Socket): Promise<void>;
|
|
35
|
+
serveStream(readable: ReadableStream<Uint8Array> | NodeJS.ReadableStream, writable?: number | import("node:net").Socket | import("@query-farm/vgi-rpc").ByteSink): Promise<void>;
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@query-farm/vgi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "TypeScript framework for extending DuckDB with user-defined functions via the Vector Gateway Interface (VGI) over Arrow IPC.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Query Farm LLC (https://query.farm)",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
],
|
|
76
76
|
"imports": {
|
|
77
77
|
"#arrow-impl": {
|
|
78
|
+
"flechette": "./src/arrow/impl-flechette/index.ts",
|
|
78
79
|
"workerd": "./src/arrow/impl-flechette/index.ts",
|
|
79
80
|
"worker": "./src/arrow/impl-flechette/index.ts",
|
|
80
81
|
"default": "./src/arrow/impl-arrowjs/index.ts"
|
|
@@ -86,12 +87,12 @@
|
|
|
86
87
|
},
|
|
87
88
|
"peerDependencies": {
|
|
88
89
|
"@query-farm/apache-arrow": "^21.1.1",
|
|
89
|
-
"@query-farm/vgi-rpc": "^0.
|
|
90
|
+
"@query-farm/vgi-rpc": "^0.14.2"
|
|
90
91
|
},
|
|
91
92
|
"devDependencies": {
|
|
92
93
|
"@duckdb/node-api": "^1.5.2-r.1",
|
|
93
94
|
"@query-farm/apache-arrow": "^21.1.1",
|
|
94
|
-
"@query-farm/vgi-rpc": "^0.
|
|
95
|
+
"@query-farm/vgi-rpc": "^0.14.2",
|
|
95
96
|
"@types/bun": "latest",
|
|
96
97
|
"typescript": "^6.0.3"
|
|
97
98
|
},
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
// Per-record-batch metadata attachment (arrow-js backend).
|
|
3
|
+
|
|
4
|
+
import { RecordBatch } from "@query-farm/apache-arrow";
|
|
5
|
+
import type { VgiBatch } from "../types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Re-emit a batch with a different per-record-batch metadata map (same schema
|
|
9
|
+
* + data, no copy). The @query-farm/apache-arrow fork's RecordBatch carries
|
|
10
|
+
* the map through IPC as the Message's `custom_metadata` field.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors vgi-rpc's arrow facade helper of the same name — needed here so a
|
|
13
|
+
* worker can bake metadata into the batch object itself (vgi-rpc's HTTP
|
|
14
|
+
* exchange dispatch drops the separate emit-metadata argument for 0-row
|
|
15
|
+
* batches; a batch-carried map survives).
|
|
16
|
+
*/
|
|
17
|
+
export function withBatchMetadata(batch: VgiBatch, metadata: Map<string, string>): VgiBatch {
|
|
18
|
+
const a = batch as unknown as RecordBatch;
|
|
19
|
+
return new RecordBatch(a.schema, a.data, metadata) as unknown as VgiBatch;
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
// Per-record-batch metadata attachment (flechette backend).
|
|
3
|
+
|
|
4
|
+
import type { VgiBatch } from "../types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Re-emit a batch with a different per-record-batch metadata map (same schema
|
|
8
|
+
* + data). Shallow-clones the flechette Table so the caller's reference is
|
|
9
|
+
* not mutated, then pins the map so that:
|
|
10
|
+
* - `batch.metadata` surfaces it to consumer code (matching arrow-js's
|
|
11
|
+
* RecordBatch.metadata getter behavior), and
|
|
12
|
+
* - the patched flechette encoder picks it up via `_vgiRecordMetadata` and
|
|
13
|
+
* emits it as the IPC Message's `custom_metadata` field.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors vgi-rpc's arrow facade helper of the same name.
|
|
16
|
+
*/
|
|
17
|
+
export function withBatchMetadata(batch: VgiBatch, metadata: Map<string, string>): VgiBatch {
|
|
18
|
+
const t = batch as any;
|
|
19
|
+
const clone = Object.assign(Object.create(Object.getPrototypeOf(t)), t);
|
|
20
|
+
if (metadata && metadata.size > 0) {
|
|
21
|
+
clone._vgiRecordMetadata = metadata;
|
|
22
|
+
clone.metadata = metadata;
|
|
23
|
+
}
|
|
24
|
+
return clone as unknown as VgiBatch;
|
|
25
|
+
}
|
package/src/arrow/index.ts
CHANGED
|
@@ -53,6 +53,8 @@ export {
|
|
|
53
53
|
emptyBatch, batchFromRows, batchFromColumns, columnFromArray,
|
|
54
54
|
iterRows, batchToScalarDict, batchToSecretDict, safeNumber, decodeDictValue,
|
|
55
55
|
filterBatch, projectSchema, projectBatch,
|
|
56
|
+
// per-record-batch metadata attachment
|
|
57
|
+
withBatchMetadata,
|
|
56
58
|
// canonical single-value read (backend-agnostic, lossless)
|
|
57
59
|
readCanonicalValue,
|
|
58
60
|
// IPC
|
package/src/catalog/read-only.ts
CHANGED
|
@@ -445,6 +445,15 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
|
|
|
445
445
|
(f.kind as string) === "table_buffering" && f.meta.sinkOrderDependent === true,
|
|
446
446
|
requires_input_batch_index:
|
|
447
447
|
(f.kind as string) === "table_buffering" && f.meta.requiresInputBatchIndex === true,
|
|
448
|
+
// Blended ("UNNEST-style") table-in-out: positional args ARE the
|
|
449
|
+
// per-row input columns (real typed args, no TABLE placeholder), so
|
|
450
|
+
// one registration serves literal / column / LATERAL call shapes.
|
|
451
|
+
// Set by defineRowTransformFunction; the C++ extension reads it to
|
|
452
|
+
// enter the in-out registration branch with real-typed args and
|
|
453
|
+
// drive the literal single-row scan-mode. False for scalars —
|
|
454
|
+
// mirrors Python's `input_from_args=False if is_scalar else ...`.
|
|
455
|
+
input_from_args:
|
|
456
|
+
f.kind === "table_in_out" && f.meta.inputFromArgs === true,
|
|
448
457
|
required_settings: meta.requiredSettings,
|
|
449
458
|
required_secrets: requiredSecrets.map((s) => ({
|
|
450
459
|
secret_type: s.secret_type,
|
package/src/client/client.ts
CHANGED
|
@@ -170,6 +170,7 @@ export class VgiClient {
|
|
|
170
170
|
tablesample_seed: ts?.seed == null ? null : BigInt(ts.seed),
|
|
171
171
|
execution_id: opts?.executionId ?? null,
|
|
172
172
|
init_opaque_data: null,
|
|
173
|
+
substream_id: null,
|
|
173
174
|
};
|
|
174
175
|
|
|
175
176
|
const initBatch = serializeInitRequest(initRequest);
|
|
@@ -123,16 +123,40 @@ function filterByArgumentTypes(
|
|
|
123
123
|
matched = r2.matched;
|
|
124
124
|
}
|
|
125
125
|
} else {
|
|
126
|
-
// Table functions: compare positional
|
|
126
|
+
// Table functions: compare declared positional specs against the
|
|
127
|
+
// argument types.
|
|
127
128
|
const posSpecs = specs
|
|
128
129
|
.filter(s => typeof s.position === "number" && !s.isTableInput)
|
|
129
130
|
.sort((a, b) => (a.position as number) - (b.position as number));
|
|
130
131
|
|
|
131
132
|
const posTypes: (VgiDataType | null)[] = [];
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
if (func.meta.inputFromArgs) {
|
|
134
|
+
// Blended ("UNNEST-style"): the positional args ARE the per-row input
|
|
135
|
+
// columns, so they are NOT on the wire (args.positional is empty).
|
|
136
|
+
// Score the declared positional specs against the INPUT_SCHEMA column
|
|
137
|
+
// types instead (coercibly, via scoreTypes), so same-arity overloads
|
|
138
|
+
// disambiguate by type — matching what DuckDB's binder resolved.
|
|
139
|
+
// Expand a trailing varargs spec across the remaining input columns
|
|
140
|
+
// (mirrors the scalar column path).
|
|
141
|
+
if (inputSchema) {
|
|
142
|
+
let varArgsSpec: ArgumentSpec | null = null;
|
|
143
|
+
for (const spec of posSpecs) {
|
|
144
|
+
if (spec.isVarargs) varArgsSpec = spec;
|
|
145
|
+
const pos = spec.position as number;
|
|
146
|
+
posTypes.push(pos < inputSchema.fields.length ? inputSchema.fields[pos].type : null);
|
|
147
|
+
}
|
|
148
|
+
if (varArgsSpec) {
|
|
149
|
+
for (let i = (varArgsSpec.position as number) + 1; i < inputSchema.fields.length; i++) {
|
|
150
|
+
posTypes.push(inputSchema.fields[i].type);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
const argsSchema = args.argumentsSchema;
|
|
156
|
+
for (let i = 0; i < args.positional.length; i++) {
|
|
157
|
+
const field = argsSchema?.fields.find(f => f.name === `positional_${i}`);
|
|
158
|
+
posTypes.push(field ? field.type : null);
|
|
159
|
+
}
|
|
136
160
|
}
|
|
137
161
|
const r = scoreTypes(posSpecs, posTypes);
|
|
138
162
|
score += r.score;
|
|
@@ -207,6 +231,30 @@ export class FunctionRegistry {
|
|
|
207
231
|
);
|
|
208
232
|
const hasVarargs = posSpecs.some(s => s.isVarargs);
|
|
209
233
|
const nonVarargs = posSpecs.filter(s => !s.isVarargs).length;
|
|
234
|
+
if (func.meta.inputFromArgs) {
|
|
235
|
+
// Blended ("UNNEST-style") overload: the positional params ARE the
|
|
236
|
+
// per-row input columns, so they are NOT on the wire
|
|
237
|
+
// (args.positional is empty). Resolve by INPUT-COLUMN count (arity)
|
|
238
|
+
// against the declared positional params instead — e.g.
|
|
239
|
+
// geo_encode(52,13) -> 2 input cols -> the 2-positional overload.
|
|
240
|
+
// Named (str-position) args still come from the wire arguments.
|
|
241
|
+
const numInputCols = context.inputSchema?.fields.length ?? 0;
|
|
242
|
+
if (hasVarargs) {
|
|
243
|
+
if (numInputCols < nonVarargs) return false;
|
|
244
|
+
} else if (numInputCols !== posSpecs.length) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
// Unknown named argument disqualifies the overload.
|
|
248
|
+
const validNamed = new Set(
|
|
249
|
+
func.argumentSpecs
|
|
250
|
+
.filter(s => typeof s.position === "string")
|
|
251
|
+
.map(s => s.position as string),
|
|
252
|
+
);
|
|
253
|
+
for (const key of args.named.keys()) {
|
|
254
|
+
if (!validNamed.has(key)) return false;
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
210
258
|
if (hasVarargs) {
|
|
211
259
|
return numArgs >= nonVarargs;
|
|
212
260
|
}
|
package/src/functions/scalar.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
type ArgumentConstraints,
|
|
45
45
|
} from "../arguments/argument-spec.js";
|
|
46
46
|
import { batchToScalarDict, batchToSecretDict, batchFromColumns, projectBatch, safeNumber } from "../util/arrow/index.js";
|
|
47
|
+
import { cacheControlMetadata, type CacheControl } from "../cache-control.js";
|
|
47
48
|
|
|
48
49
|
// ============================================================================
|
|
49
50
|
// Scalar Bind Parameters
|
|
@@ -181,6 +182,15 @@ export interface ScalarFunctionConfig<
|
|
|
181
182
|
maxWorkers?: number;
|
|
182
183
|
requiredSettings?: string[];
|
|
183
184
|
requiredSecrets?: string[];
|
|
185
|
+
/**
|
|
186
|
+
* Result-cache opt-in: when set, this CacheControl's `vgi.cache.*` metadata
|
|
187
|
+
* rides every output batch's custom metadata (per-batch — NOT the schema,
|
|
188
|
+
* which the IPC stream fixes at open), so the extension can memoize the
|
|
189
|
+
* scalar's output per distinct input value. A pure, deterministic scalar
|
|
190
|
+
* only — advertising this on a non-pure scalar serves stale rows. Mirrors
|
|
191
|
+
* vgi-python's `ScalarFunction.CACHE_CONTROL`.
|
|
192
|
+
*/
|
|
193
|
+
cacheControl?: CacheControl;
|
|
184
194
|
}
|
|
185
195
|
|
|
186
196
|
export function defineScalarFunction<
|
|
@@ -394,7 +404,13 @@ export function defineScalarFunction<
|
|
|
394
404
|
);
|
|
395
405
|
}
|
|
396
406
|
|
|
397
|
-
|
|
407
|
+
// Result-cache opt-in: a scalar declaring cacheControl rides its
|
|
408
|
+
// vgi.cache.* keys on the emit path's per-batch custom metadata so
|
|
409
|
+
// the extension can memoize the output per distinct input value.
|
|
410
|
+
out.emit(
|
|
411
|
+
outputBatch,
|
|
412
|
+
config.cacheControl ? cacheControlMetadata(config.cacheControl) : undefined,
|
|
413
|
+
);
|
|
398
414
|
},
|
|
399
415
|
};
|
|
400
416
|
},
|