@query-farm/vgi 0.22.0 → 0.24.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@query-farm/vgi",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
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)",
@@ -87,12 +87,12 @@
87
87
  },
88
88
  "peerDependencies": {
89
89
  "@query-farm/apache-arrow": "^21.1.1",
90
- "@query-farm/vgi-rpc": "^0.15.4"
90
+ "@query-farm/vgi-rpc": "^0.17.0"
91
91
  },
92
92
  "devDependencies": {
93
93
  "@duckdb/node-api": "^1.5.2-r.1",
94
94
  "@query-farm/apache-arrow": "^21.1.1",
95
- "@query-farm/vgi-rpc": "^0.15.4",
95
+ "@query-farm/vgi-rpc": "^0.17.0",
96
96
  "@types/bun": "latest",
97
97
  "typescript": "^6.0.3"
98
98
  },
@@ -173,4 +173,22 @@ export interface CatalogDescriptor {
173
173
  * advertises a null `source_url`.
174
174
  */
175
175
  sourceUrl?: string;
176
+ /**
177
+ * Functions this catalog asks the client to *additionally* publish into its
178
+ * global (DuckDB `system.main`) function namespace, on top of the normal
179
+ * schema-qualified registration. Surfaced as IPC-serialized `FunctionInfo`
180
+ * records on `catalog_attach.global_functions`.
181
+ *
182
+ * Every entry must also be declared in exactly one of this catalog's
183
+ * schemas — the `FunctionInfo.schema_name` it carries is the bind-dispatch
184
+ * key, not a sentinel. Mirrors vgi-python's `Catalog.global_functions`.
185
+ */
186
+ globalFunctions?: VgiFunction[];
187
+ /**
188
+ * Prefix applied client-side to every `globalFunctions` entry's name when it
189
+ * is published globally (e.g. prefix `vgi_example` + `global_scalar` →
190
+ * `vgi_example_global_scalar`). The advertised `FunctionInfo.name` stays
191
+ * unprefixed. Surfaced as `catalog_attach.global_function_prefix`.
192
+ */
193
+ globalFunctionPrefix?: string;
176
194
  }
@@ -17,6 +17,7 @@ import {
17
17
  type IndexConstraintType,
18
18
  type CopyFromFormatInfo,
19
19
  encodeAttachCatalogInfo,
20
+ encodeFunctionInfo,
20
21
  } from "./interface.js";
21
22
  import type { CatalogDescriptor, SchemaDescriptor, TableDescriptor, ViewDescriptor, MacroDescriptor, SettingDescriptor, SecretTypeDescriptor, ForeignKeyDef, DefaultValue } from "./descriptors.js";
22
23
  import { serializeColumnStatistics } from "../util/statistics.js";
@@ -119,6 +120,11 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
119
120
  // Advertise true so DuckDB routes catalog_table_column_statistics_get
120
121
  // for tables whose TableInfo.supports_column_statistics is also true.
121
122
  supports_column_statistics: true,
123
+ // Functions the client should ALSO publish into its global (system.main)
124
+ // namespace, under `global_function_prefix` + "_" + name. Empty unless
125
+ // the descriptor opts in via `globalFunctions`.
126
+ global_functions: this._globalFunctionInfos().map(encodeFunctionInfo),
127
+ global_function_prefix: this._descriptor.globalFunctionPrefix ?? "",
122
128
  resolved_data_version: null,
123
129
  resolved_implementation_version: null,
124
130
  };
@@ -374,7 +380,18 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
374
380
  if (t === "AGGREGATE_FUNCTION") return (f.kind as string) === "aggregate";
375
381
  return true;
376
382
  })
377
- .map((f) => {
383
+ .map((f) => this._functionToInfo(f, name));
384
+ }
385
+
386
+ /**
387
+ * Build the wire `FunctionInfo` for one declared function.
388
+ *
389
+ * `schemaName` is the schema the function actually lives in — it is the
390
+ * bind-dispatch key, so a function advertised through more than one route
391
+ * (schema contents *and* `global_functions`) must carry the same value in
392
+ * both. Shared by `schemaContentsFunctions` and `_globalFunctionInfos`.
393
+ */
394
+ private _functionToInfo(f: VgiFunction, name: string): FunctionInfo {
378
395
  const meta = resolveMetadata(f);
379
396
  const argSchema = argumentSpecsToSchema(f.argumentSpecs);
380
397
  const argBytes = serializeSchema(argSchema);
@@ -474,7 +491,39 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
474
491
  secret_name: s.secret_name,
475
492
  })),
476
493
  };
477
- });
494
+ }
495
+
496
+ /**
497
+ * Build `FunctionInfo` records for `CatalogDescriptor.globalFunctions`.
498
+ *
499
+ * Each entry's `schema_name` is the schema it actually lives in — the
500
+ * globally visible name is derived client-side from
501
+ * `global_function_prefix`, so `name` stays unprefixed here. An entry not
502
+ * declared in any schema is a descriptor bug, so it throws rather than
503
+ * advertising a function no bind can resolve. Mirrors vgi-python's
504
+ * `_global_function_infos`.
505
+ */
506
+ private _globalFunctionInfos(): FunctionInfo[] {
507
+ const globals = this._descriptor.globalFunctions ?? [];
508
+ if (globals.length === 0) return [];
509
+
510
+ const schemaOf = new Map<VgiFunction, string>();
511
+ for (const sch of this._descriptor.schemas) {
512
+ for (const fn of sch.functions ?? []) {
513
+ if (!schemaOf.has(fn)) schemaOf.set(fn, sch.name);
514
+ }
515
+ }
516
+
517
+ return globals.map((fn) => {
518
+ const schemaName = schemaOf.get(fn);
519
+ if (schemaName === undefined) {
520
+ throw new Error(
521
+ `Catalog '${this._descriptor.name}': '${fn.meta.name}' is listed in ` +
522
+ `globalFunctions but is not declared in any schema.`,
523
+ );
524
+ }
525
+ return this._functionToInfo(fn, schemaName);
526
+ });
478
527
  }
479
528
 
480
529
  override schemaContentsMacros(
@@ -628,6 +628,10 @@ export class VgiClient {
628
628
  comment: inner.comment ?? null,
629
629
  tags: deserializeTags(inner.tags),
630
630
  supports_column_statistics: inner.supports_column_statistics ?? false,
631
+ global_functions: inner.global_functions
632
+ ? (Array.isArray(inner.global_functions) ? inner.global_functions : [...inner.global_functions]).map(toUint8Array)
633
+ : [],
634
+ global_function_prefix: inner.global_function_prefix ?? "",
631
635
  resolved_data_version: inner.resolved_data_version ?? null,
632
636
  resolved_implementation_version: inner.resolved_implementation_version ?? null,
633
637
  };
@@ -2,7 +2,7 @@
2
2
  // GENERATED by vgi.codegen.ts_client. DO NOT EDIT BY HAND.
3
3
  //
4
4
  // Generator: vgi-gen-ts-client v1
5
- // Content hash: 80ec56060e88
5
+ // Content hash: e0862da99312
6
6
  //
7
7
  // To regenerate:
8
8
  // uv run --project ~/Development/vgi-python vgi-gen-ts-client \
@@ -384,6 +384,8 @@ export interface CatalogAttachResult {
384
384
  comment?: string | null;
385
385
  tags: Record<string, string>;
386
386
  supports_column_statistics: boolean;
387
+ global_functions: Uint8Array[];
388
+ global_function_prefix: string;
387
389
  resolved_data_version?: string | null;
388
390
  resolved_implementation_version?: string | null;
389
391
  }
@@ -2,7 +2,7 @@
2
2
  // GENERATED by vgi.codegen.ts_schemas. DO NOT EDIT BY HAND.
3
3
  //
4
4
  // Generator: vgi-gen-ts-schemas v2
5
- // Content hash: 214d149e8f9c
5
+ // Content hash: ae533ce2196c
6
6
  //
7
7
  // To regenerate:
8
8
  // uv run --project ~/Development/vgi-python vgi-gen-ts-schemas \
@@ -265,6 +265,8 @@ export const CatalogAttachResultSchema = schema([
265
265
  field("comment", utf8(), true),
266
266
  field("tags", map(field("key", utf8(), false), field("value", utf8(), true), false), false),
267
267
  field("supports_column_statistics", bool(), false),
268
+ field("global_functions", list(field("item", binary(), true)), false),
269
+ field("global_function_prefix", utf8(), false),
268
270
  field("resolved_data_version", utf8(), true),
269
271
  field("resolved_implementation_version", utf8(), true),
270
272
  ]);
@@ -41,8 +41,20 @@ import {
41
41
  } from "./shared.js";
42
42
 
43
43
  export function registerAggregateMethods(protocol: Protocol, registry: FunctionRegistry): void {
44
- function resolveAggregate(name: string): AggregateFunctionConfig<any, any> {
45
- const func = registry.get(name, { arguments: new Arguments(), inputSchema: null, isScalar: false }) as any;
44
+ // `schemaName` scopes resolution to the schema the caller named. Every
45
+ // aggregate RPC carries it (protocol 1.2.0), so an aggregate name declared in
46
+ // two schemas dispatches to the one the request names rather than colliding
47
+ // on the flat by-name index. `null` (no schema) keeps the cross-schema lookup.
48
+ function resolveAggregate(
49
+ name: string,
50
+ schemaName?: string | null,
51
+ ): AggregateFunctionConfig<any, any> {
52
+ const func = registry.get(name, {
53
+ arguments: new Arguments(),
54
+ inputSchema: null,
55
+ isScalar: false,
56
+ schemaName: schemaName ?? null,
57
+ }) as any;
46
58
  if (!func || func.kind !== "aggregate") {
47
59
  throw new Error(`Function '${name}' is not an aggregate function`);
48
60
  }
@@ -58,7 +70,7 @@ export function registerAggregateMethods(protocol: Protocol, registry: FunctionR
58
70
  handler: async (params) => {
59
71
  const innerParams = unwrapRequest(params.request);
60
72
  const functionName: string = innerParams.function_name;
61
- const cfg = resolveAggregate(functionName);
73
+ const cfg = resolveAggregate(functionName, innerParams.schema_name ?? null);
62
74
  const argBytes = toUint8Array(innerParams.arguments);
63
75
  const args = deserializeArguments(argBytes);
64
76
  const inputSchema = innerParams.input_schema
@@ -109,7 +121,7 @@ export function registerAggregateMethods(protocol: Protocol, registry: FunctionR
109
121
  const innerParams = unwrapRequest(params.request);
110
122
  const functionName: string = innerParams.function_name;
111
123
  const executionId = toUint8Array(innerParams.execution_id);
112
- const cfg = resolveAggregate(functionName);
124
+ const cfg = resolveAggregate(functionName, innerParams.schema_name ?? null);
113
125
  const batch = readSingleBatch(toUint8Array(innerParams.input_batch));
114
126
  if (!batch || batch.numRows === 0) return wrapResult({}, makeSchema([]));
115
127
 
@@ -176,7 +188,7 @@ export function registerAggregateMethods(protocol: Protocol, registry: FunctionR
176
188
  const innerParams = unwrapRequest(params.request);
177
189
  const functionName: string = innerParams.function_name;
178
190
  const executionId = toUint8Array(innerParams.execution_id);
179
- const cfg = resolveAggregate(functionName);
191
+ const cfg = resolveAggregate(functionName, innerParams.schema_name ?? null);
180
192
  const batch = readSingleBatch(toUint8Array(innerParams.merge_batch));
181
193
  if (!batch || batch.numRows === 0) return wrapResult({}, makeSchema([]));
182
194
 
@@ -225,7 +237,7 @@ export function registerAggregateMethods(protocol: Protocol, registry: FunctionR
225
237
  const innerParams = unwrapRequest(params.request);
226
238
  const functionName: string = innerParams.function_name;
227
239
  const executionId = toUint8Array(innerParams.execution_id);
228
- const cfg = resolveAggregate(functionName);
240
+ const cfg = resolveAggregate(functionName, innerParams.schema_name ?? null);
229
241
  const outputSchema = deserializeSchema(toUint8Array(innerParams.output_schema));
230
242
  const gidBatch = readSingleBatch(toUint8Array(innerParams.group_ids_batch));
231
243
  const groupIds: bigint[] = [];
@@ -108,6 +108,12 @@ export function registerCatalogAdminMethods(protocol: Protocol, getCatalog: GetC
108
108
  // is also true. Catalogs that never serve column stats can override
109
109
  // this in attach() via CatalogAttachResult.supports_column_statistics.
110
110
  supports_column_statistics: result.supports_column_statistics ?? true,
111
+ // Protocol 1.3.0: functions the worker asks the client to publish into
112
+ // its global namespace. Empty unless the catalog advertises them —
113
+ // but the columns must still be present, since the extension pins the
114
+ // full CatalogAttachResult field list on the response schema.
115
+ global_functions: result.global_functions ?? [],
116
+ global_function_prefix: result.global_function_prefix ?? "",
111
117
  resolved_data_version: result.resolved_data_version ?? null,
112
118
  resolved_implementation_version: result.resolved_implementation_version ?? null,
113
119
  }, CatalogAttachResultSchema);
@@ -43,11 +43,19 @@ export function registerTableBufferingMethods(
43
43
  registry: FunctionRegistry,
44
44
  signingKey?: Uint8Array,
45
45
  ): void {
46
- function resolveBuffering(name: string): TableBufferingVgiFunction {
46
+ // `schemaName` scopes resolution to the schema the caller named. The
47
+ // buffering process/combine RPCs carry it (protocol 1.2.0), so a name
48
+ // declared in two schemas resolves to the one the request names rather than
49
+ // colliding on the flat by-name index. `null` keeps the cross-schema lookup.
50
+ function resolveBuffering(
51
+ name: string,
52
+ schemaName?: string | null,
53
+ ): TableBufferingVgiFunction {
47
54
  const func = registry.get(name, {
48
55
  arguments: new Arguments(),
49
56
  inputSchema: null,
50
57
  isScalar: false,
58
+ schemaName: schemaName ?? null,
51
59
  }) as any;
52
60
  if (!func || func.kind !== "table_buffering") {
53
61
  throw new Error(`Function '${name}' is not a table_buffering function`);
@@ -75,7 +83,7 @@ export function registerTableBufferingMethods(
75
83
  : rawAttach;
76
84
  const transactionId = inner.transaction_id ? toUint8Array(inner.transaction_id) : null;
77
85
 
78
- const func = resolveBuffering(functionName);
86
+ const func = resolveBuffering(functionName, inner.schema_name ?? null);
79
87
  const bound = new BoundStorage(defaultStorage, executionId);
80
88
  const payload = await bound.stateGet(
81
89
  FrameworkNS.BUFFERING_INIT,