@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/dist/catalog/descriptors.d.ts +18 -0
- package/dist/catalog/read-only.d.ts +20 -0
- package/dist/client-entry.js +5 -1
- package/dist/client-entry.js.map +5 -5
- package/dist/generated/vgi-client.d.ts +2 -0
- package/dist/index.js +94 -60
- package/dist/index.js.map +9 -9
- package/dist/serve-entry.js +20 -10
- package/dist/serve-entry.js.map +7 -7
- package/dist/worker-cf-entry.js +92 -61
- package/dist/worker-cf-entry.js.map +8 -8
- package/package.json +3 -3
- package/src/catalog/descriptors.ts +18 -0
- package/src/catalog/read-only.ts +51 -2
- package/src/client/client.ts +4 -0
- package/src/generated/vgi-client.ts +3 -1
- package/src/generated/vgi-protocol-schemas.ts +3 -1
- package/src/protocol/handlers/aggregate.ts +18 -6
- package/src/protocol/handlers/catalog/admin.ts +6 -0
- package/src/protocol/handlers/table-buffering.ts +10 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@query-farm/vgi",
|
|
3
|
-
"version": "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.
|
|
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.
|
|
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
|
}
|
package/src/catalog/read-only.ts
CHANGED
|
@@ -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(
|
package/src/client/client.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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,
|