@query-farm/vgi 0.13.0 → 0.15.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.13.0",
3
+ "version": "0.15.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)",
@@ -75,12 +75,12 @@
75
75
  },
76
76
  "peerDependencies": {
77
77
  "@query-farm/apache-arrow": "^21.1.1",
78
- "@query-farm/vgi-rpc": "^0.10.0"
78
+ "@query-farm/vgi-rpc": "^0.12.0"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@duckdb/node-api": "^1.5.2-r.1",
82
82
  "@query-farm/apache-arrow": "^21.1.1",
83
- "@query-farm/vgi-rpc": "^0.10.0",
83
+ "@query-farm/vgi-rpc": "^0.12.0",
84
84
  "@types/bun": "latest",
85
85
  "typescript": "^6.0.3"
86
86
  },
@@ -31,6 +31,7 @@ import type {
31
31
  CatalogAttachResult,
32
32
  CatalogInfo,
33
33
  FunctionInfo,
34
+ MacroInfo,
34
35
  SchemaInfo,
35
36
  TableInfo,
36
37
  ViewInfo,
@@ -104,6 +105,32 @@ function typeToString(type: VgiDataType): string {
104
105
  return s ?? String(type.typeId);
105
106
  }
106
107
 
108
+ /**
109
+ * Raw Arrow type string (e.g. `int64`, `string`, `double`) matching pyarrow's
110
+ * `str(type)`. Unlike {@link typeToString} (DuckDB-ish names used for table/view
111
+ * columns), macro arg types are carried verbatim in `describe.json` — the shared
112
+ * landing page maps Arrow → DuckDB client-side — so they must be byte-identical
113
+ * to what the Python reference producer emits for cross-language parity.
114
+ */
115
+ function arrowTypeString(type: VgiDataType): string {
116
+ if (isNull(type)) return "null";
117
+ if (isBool(type)) return "bool";
118
+ if (isUtf8(type)) return "string";
119
+ if (isBinary(type)) return "binary";
120
+ if (isInt(type)) {
121
+ const bw = (type as { bitWidth?: number }).bitWidth ?? 32;
122
+ const signed =
123
+ (type as { isSigned?: boolean }).isSigned ?? (type as { signed?: boolean }).signed ?? true;
124
+ return `${signed ? "" : "u"}int${bw}`;
125
+ }
126
+ if (isFloat(type)) {
127
+ const precision = (type as { precision?: number }).precision ?? 2;
128
+ return precision === 0 ? "halffloat" : precision === 1 ? "float" : "double";
129
+ }
130
+ const s = (type as { toString?: () => string }).toString?.();
131
+ return (s ?? String(type.typeId)).toLowerCase();
132
+ }
133
+
107
134
  function fieldMeta(field: VgiField, key: string): string | undefined {
108
135
  return field.metadata.get(key);
109
136
  }
@@ -189,6 +216,85 @@ function functionArgs(fn: FunctionInfo): ArgJson[] {
189
216
  return args;
190
217
  }
191
218
 
219
+ // ---------------------------------------------------------------------------
220
+ // Macro mapping
221
+ // ---------------------------------------------------------------------------
222
+
223
+ // A scalar macro is invoked exactly like a scalar function in SQL, and a table
224
+ // macro like a table function; surface them in the same buckets so the landing
225
+ // page lists a catalog's full callable surface (VGI workers commonly expose
226
+ // their "functions" as declarative macros). Mirrors Python `_macro_display_type`.
227
+ function macroDisplayType(m: MacroInfo): "scalar" | "table" {
228
+ return String(m.macro_type).toUpperCase() === "SCALAR" ? "scalar" : "table";
229
+ }
230
+
231
+ /** JSON-encode a decoded macro default scalar, matching Python's `json.dumps`. */
232
+ function macroDefaultJson(value: unknown): string {
233
+ // int64 defaults decode to BigInt, which JSON.stringify rejects; a bigint's
234
+ // decimal string is already its JSON integer encoding (0n -> "0").
235
+ if (typeof value === "bigint") return value.toString();
236
+ try {
237
+ return JSON.stringify(value);
238
+ } catch {
239
+ return String(value);
240
+ }
241
+ }
242
+
243
+ function macroArgs(m: MacroInfo): ArgJson[] {
244
+ // Defaulted parameters are optional and callable by name in DuckDB, so we
245
+ // present them as named args (with their default); the rest are positional.
246
+ const defaults = new Map<string, unknown>();
247
+ const defBytes = m.parameter_default_values;
248
+ if (defBytes && defBytes.length > 0) {
249
+ try {
250
+ const batch = deserializeBatch(defBytes);
251
+ for (const field of batch.schema.fields) {
252
+ try {
253
+ defaults.set(field.name, batch.getChild(field.name)?.get(0));
254
+ } catch {
255
+ // skip an undecodable default column
256
+ }
257
+ }
258
+ } catch {
259
+ // no usable defaults
260
+ }
261
+ }
262
+
263
+ const schema = readSchemaSafe(m.arguments_schema);
264
+ const fields = schema ? schema.fields : null;
265
+ const fieldByName = new Map<string, VgiField>();
266
+ if (fields) for (const f of fields) fieldByName.set(f.name, f);
267
+ const names = fields ? fields.map((f) => f.name) : m.parameters;
268
+
269
+ const args: ArgJson[] = [];
270
+ for (const name of names) {
271
+ const field = fieldByName.get(name);
272
+ // Macro parameters are untyped unless a typed default pins them; show ANY
273
+ // rather than the Arrow null placeholder.
274
+ const arg: ArgJson =
275
+ field !== undefined && !isNull(field.type)
276
+ ? { name, type: arrowTypeString(field.type) }
277
+ : { name, type: "ANY" };
278
+ const doc = field !== undefined ? fieldMeta(field, VGI_DOC_KEY) : undefined;
279
+ if (doc) arg.desc = doc;
280
+ if (defaults.has(name)) {
281
+ arg.named = true;
282
+ arg.default = macroDefaultJson(defaults.get(name));
283
+ }
284
+ args.push(arg);
285
+ }
286
+ return args;
287
+ }
288
+
289
+ function macroToObject(m: MacroInfo): Record<string, unknown> {
290
+ return {
291
+ name: m.name,
292
+ type: macroDisplayType(m),
293
+ doc: m.comment ?? "",
294
+ args: macroArgs(m),
295
+ };
296
+ }
297
+
192
298
  // ---------------------------------------------------------------------------
193
299
  // Catalog contents
194
300
  // ---------------------------------------------------------------------------
@@ -221,6 +327,18 @@ async function schemaFunctions(iface: CatalogInterface, aod: Uint8Array, name: s
221
327
  return out;
222
328
  }
223
329
 
330
+ async function schemaMacros(iface: CatalogInterface, aod: Uint8Array, name: string): Promise<MacroInfo[]> {
331
+ const out: MacroInfo[] = [];
332
+ for (const kind of ["SCALAR_MACRO", "TABLE_MACRO"]) {
333
+ try {
334
+ out.push(...(await iface.schemaContentsMacros(aod, name, kind)));
335
+ } catch {
336
+ // best-effort — keep whatever we collected
337
+ }
338
+ }
339
+ return out;
340
+ }
341
+
224
342
  interface Counts {
225
343
  schemas: number;
226
344
  tables: number;
@@ -246,6 +364,7 @@ async function buildSchemas(
246
364
  const tablesRaw = (await schemaContents(iface, aod, si.name, "TABLE")) as TableInfo[];
247
365
  const viewsRaw = (await schemaContents(iface, aod, si.name, "VIEW")) as ViewInfo[];
248
366
  const funcsRaw = await schemaFunctions(iface, aod, si.name);
367
+ const macrosRaw = await schemaMacros(iface, aod, si.name);
249
368
 
250
369
  const tables = tablesRaw.map((t) => {
251
370
  const schema = readSchemaSafe(t.columns);
@@ -259,24 +378,33 @@ async function buildSchemas(
259
378
  def: v.definition,
260
379
  }));
261
380
 
262
- const functions = funcsRaw
263
- .slice()
264
- .sort((a, b) =>
265
- String(a.function_type).localeCompare(String(b.function_type)) || a.name.localeCompare(b.name),
266
- )
267
- .map((fn) => {
268
- const obj: Record<string, unknown> = {
269
- name: fn.name,
270
- type: functionDisplayType(fn),
271
- doc: fn.description ?? "",
272
- args: functionArgs(fn),
273
- };
274
- const ret = functionReturns(fn);
275
- if (ret) obj.returns = ret;
276
- return obj;
277
- });
278
-
279
- schemas.push({ name: si.name, tables, views, functions });
381
+ const functions: Record<string, unknown>[] = funcsRaw.map((fn) => {
382
+ const obj: Record<string, unknown> = {
383
+ name: fn.name,
384
+ type: functionDisplayType(fn),
385
+ doc: fn.description ?? "",
386
+ args: functionArgs(fn),
387
+ };
388
+ const ret = functionReturns(fn);
389
+ if (ret) obj.returns = ret;
390
+ return obj;
391
+ });
392
+ // Fold macros into the same scalar/table buckets (VGI catalogs commonly
393
+ // expose their callable surface as declarative macros).
394
+ functions.push(...macrosRaw.map((m) => macroToObject(m)));
395
+ // Deterministic ordering across functions + macros, matching the Python
396
+ // reference producer's `sort(key=(type, name))`.
397
+ functions.sort(
398
+ (a, b) =>
399
+ String(a.type).localeCompare(String(b.type)) || String(a.name).localeCompare(String(b.name)),
400
+ );
401
+
402
+ const schemaObj: Record<string, unknown> = { name: si.name };
403
+ if (si.comment) schemaObj.doc = si.comment;
404
+ schemaObj.tables = tables;
405
+ schemaObj.views = views;
406
+ schemaObj.functions = functions;
407
+ schemas.push(schemaObj);
280
408
  counts.schemas += 1;
281
409
  counts.tables += tables.length;
282
410
  counts.views += views.length;