@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.
Files changed (38) hide show
  1. package/dist/arrow/impl-arrowjs/index.d.ts +1 -0
  2. package/dist/arrow/impl-arrowjs/metadata.d.ts +12 -0
  3. package/dist/arrow/impl-flechette/index.d.ts +1 -0
  4. package/dist/arrow/impl-flechette/metadata.d.ts +13 -0
  5. package/dist/arrow/index.d.ts +1 -1
  6. package/dist/client-entry.js +15 -4
  7. package/dist/client-entry.js.map +9 -8
  8. package/dist/functions/scalar.d.ts +10 -0
  9. package/dist/functions/table-in-out.d.ts +121 -0
  10. package/dist/functions/types.d.ts +12 -0
  11. package/dist/generated/vgi-client.d.ts +2 -0
  12. package/dist/index.core.d.ts +1 -1
  13. package/dist/index.js +295 -11
  14. package/dist/index.js.map +16 -15
  15. package/dist/protocol/types.d.ts +12 -0
  16. package/dist/serve-entry.js +13 -3
  17. package/dist/serve-entry.js.map +8 -7
  18. package/dist/worker-cf-entry.js +173 -16
  19. package/dist/worker-cf-entry.js.map +14 -13
  20. package/dist/worker.d.ts +1 -1
  21. package/package.json +4 -3
  22. package/src/arrow/impl-arrowjs/index.ts +1 -0
  23. package/src/arrow/impl-arrowjs/metadata.ts +20 -0
  24. package/src/arrow/impl-flechette/index.ts +1 -0
  25. package/src/arrow/impl-flechette/metadata.ts +25 -0
  26. package/src/arrow/index.ts +2 -0
  27. package/src/catalog/read-only.ts +9 -0
  28. package/src/client/client.ts +1 -0
  29. package/src/functions/registry.ts +53 -5
  30. package/src/functions/scalar.ts +17 -1
  31. package/src/functions/table-in-out.ts +480 -4
  32. package/src/functions/types.ts +12 -0
  33. package/src/generated/vgi-client.ts +3 -1
  34. package/src/generated/vgi-protocol-schemas.ts +2 -1
  35. package/src/index.core.ts +11 -1
  36. package/src/protocol/serializers/init.ts +7 -0
  37. package/src/protocol/types.ts +12 -0
  38. 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.18.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.13.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.13.0",
95
+ "@query-farm/vgi-rpc": "^0.14.2",
95
96
  "@types/bun": "latest",
96
97
  "typescript": "^6.0.3"
97
98
  },
@@ -41,6 +41,7 @@ export {
41
41
  export { filterBatch } from "./filter.js";
42
42
  export { projectSchema, projectBatch } from "./project.js";
43
43
  export { readCanonicalValue } from "./canonical.js";
44
+ export { withBatchMetadata } from "./metadata.js";
44
45
 
45
46
  // IPC
46
47
  export {
@@ -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
+ }
@@ -41,6 +41,7 @@ export {
41
41
  export { filterBatch } from "./filter.js";
42
42
  export { projectSchema, projectBatch } from "./project.js";
43
43
  export { readCanonicalValue } from "./canonical.js";
44
+ export { withBatchMetadata } from "./metadata.js";
44
45
 
45
46
  // IPC
46
47
  export {
@@ -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
+ }
@@ -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
@@ -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,
@@ -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 arg types
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
- const argsSchema = args.argumentsSchema;
133
- for (let i = 0; i < args.positional.length; i++) {
134
- const field = argsSchema?.fields.find(f => f.name === `positional_${i}`);
135
- posTypes.push(field ? field.type : null);
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
  }
@@ -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
- out.emit(outputBatch);
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
  },