@query-farm/vgi 0.33.0 → 0.35.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.33.0",
3
+ "version": "0.35.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)",
@@ -91,12 +91,12 @@
91
91
  },
92
92
  "peerDependencies": {
93
93
  "@query-farm/apache-arrow": "^21.1.1",
94
- "@query-farm/vgi-rpc": ">=0.25.2 <0.26.0"
94
+ "@query-farm/vgi-rpc": ">=0.25.4 <0.26.0"
95
95
  },
96
96
  "devDependencies": {
97
97
  "@duckdb/node-api": "^1.5.5-r.4",
98
98
  "@query-farm/apache-arrow": "^21.1.1",
99
- "@query-farm/vgi-rpc": "^0.25.2",
99
+ "@query-farm/vgi-rpc": "^0.25.4",
100
100
  "@types/bun": "latest",
101
101
  "typescript": "^7.0.2"
102
102
  },
@@ -53,11 +53,20 @@ export class Arguments {
53
53
 
54
54
  /**
55
55
  * Unwrap an Arrow Scalar to a plain JS value.
56
+ *
57
+ * A `Date` is returned as-is. Arguments are decoded to their RICH values
58
+ * (deserializeArguments runs every value through the codec), and a JS `Date`
59
+ * is the rich value of date32/date64 — `valueOf()` would turn it into epoch
60
+ * MILLISECONDS, while a bare number in a date32 column means DAYS. So a DATE
61
+ * argument read through `get()` (every typed `params.args.x`, every scalar
62
+ * const) and written back out as a DATE came back 86,400,000x too far out:
63
+ * '2024-01-15' as 480510-12-09 on arrow-js, and an invalid Date on flechette.
56
64
  */
57
65
  function unwrapScalar(val: any): any {
58
66
  if (val === null || val === undefined) return null;
59
67
  // If it's already a primitive, return it
60
68
  if (typeof val !== "object") return val;
69
+ if (val instanceof Date) return val;
61
70
  // Arrow Scalar objects have a valueOf() method
62
71
  if (typeof val.valueOf === "function") {
63
72
  let v: any;
@@ -27,6 +27,7 @@
27
27
  import {
28
28
  Struct,
29
29
  List,
30
+ FixedSizeList,
30
31
  DataType,
31
32
  DateUnit,
32
33
  makeData,
@@ -54,6 +55,7 @@ export function writeCanonicalColumn(
54
55
 
55
56
  function buildColumnData(values: unknown[], type: DataType): any {
56
57
  if (DataType.isList(type)) return buildListData(values, type as List);
58
+ if (DataType.isFixedSizeList(type)) return buildFixedSizeListData(values, type as FixedSizeList);
57
59
  if (DataType.isMap(type)) return buildMapData(values, type as Map_);
58
60
  if (DataType.isStruct(type)) return buildStructData(values, type as Struct);
59
61
  if (DataType.isDecimal(type)) return buildDecimalData(values, type);
@@ -200,6 +202,48 @@ function buildListData(values: unknown[], listType: List): any {
200
202
  });
201
203
  }
202
204
 
205
+ /**
206
+ * FixedSizeList (DuckDB's ARRAY) is built by hand, like List, so its items go
207
+ * through buildColumnData. Handing it to vectorFromArray fed arrow-js's own
208
+ * child builders CANONICAL values they read in other units: a date32
209
+ * day-number as epoch-ms (DATE[2], STRUCT(d DATE)[1], MAP(DATE, DATE)[1] and
210
+ * DATE[][1] all came back as 1970-01-01), and a bigint timestamp or unscaled
211
+ * decimal threw.
212
+ */
213
+ function buildFixedSizeListData(values: unknown[], type: FixedSizeList): any {
214
+ const childField = type.children[0];
215
+ const listSize = type.listSize;
216
+ const numRows = values.length;
217
+ const allItems: unknown[] = [];
218
+ const nullBitmap = new Uint8Array(Math.ceil(numRows / 8));
219
+ let nullCount = 0;
220
+ for (let i = 0; i < numRows; i++) {
221
+ const row = values[i];
222
+ if (row == null) {
223
+ // A null row still occupies listSize child slots.
224
+ nullCount++;
225
+ for (let j = 0; j < listSize; j++) allItems.push(null);
226
+ continue;
227
+ }
228
+ nullBitmap[i >> 3] |= 1 << (i & 7);
229
+ const items = Array.isArray(row) ? row : Array.from(row as Iterable<unknown>);
230
+ if (items.length !== listSize) {
231
+ throw new TypeError(
232
+ `fixed_size_list[${listSize}]: row ${i} has ${items.length} items`,
233
+ );
234
+ }
235
+ for (const item of items) allItems.push(item);
236
+ }
237
+ const childData = allItems.length > 0
238
+ ? buildColumnData(allItems, childField.type)
239
+ : emptyDataForType(childField.type);
240
+ return makeData({
241
+ type, length: numRows, nullCount,
242
+ nullBitmap: nullCount > 0 ? nullBitmap : undefined,
243
+ child: childData,
244
+ });
245
+ }
246
+
203
247
  function buildMapData(values: unknown[], mapType: Map_): any {
204
248
  const numRows = values.length;
205
249
  const entriesField = mapType.children[0];
@@ -285,6 +329,12 @@ function emptyDataForType(type: DataType): any {
285
329
  valueOffsets: new Int32Array([0]),
286
330
  });
287
331
  }
332
+ if (DataType.isFixedSizeList(type)) {
333
+ return makeData({
334
+ type, length: 0, nullCount: 0,
335
+ child: emptyDataForType((type as FixedSizeList).children[0].type),
336
+ });
337
+ }
288
338
  if (DataType.isStruct(type)) {
289
339
  const children = (type as any).children.map((cf: Field) => emptyDataForType(cf.type));
290
340
  return makeData({ type, length: 0, nullCount: 0, children });
@@ -118,12 +118,29 @@ export function prepareForFlechette(type: VgiDataType, values: unknown[]): unkno
118
118
  });
119
119
  }
120
120
  if (tid === 17) {
121
+ // The keys and values need the same preparation as a column of their
122
+ // type: a date32 key/value is a canonical day-number, which flechette's
123
+ // builder reads as epoch-ms, so MAP {DATE: DATE} came back as
124
+ // {1970-01-01=1970-01-01} unless it is turned into a JS Date first.
125
+ const entries = (type as any).children?.[0]?.type?.children;
126
+ const keyType = entries?.[0]?.type as VgiDataType | undefined;
127
+ const valueType = entries?.[1]?.type as VgiDataType | undefined;
128
+ const prepKey = keyType !== undefined && needsPrepare(keyType);
129
+ const prepValue = valueType !== undefined && needsPrepare(valueType);
121
130
  return values.map((v) => {
122
131
  if (v == null) return null;
123
- if (v instanceof Map) return v;
124
- if (Array.isArray(v)) return new Map(v as Array<[unknown, unknown]>);
125
- if (typeof v === "object") return new Map(Object.entries(v as any));
126
- return v;
132
+ let pairs: Array<[unknown, unknown]>;
133
+ if (v instanceof Map) pairs = Array.from(v.entries());
134
+ else if (Array.isArray(v)) pairs = v as Array<[unknown, unknown]>;
135
+ else if (typeof v === "object") pairs = Object.entries(v as any);
136
+ else return v;
137
+ if (prepKey || prepValue) {
138
+ pairs = pairs.map(([k, x]) => [
139
+ prepKey ? prepareForFlechette(keyType!, [k])[0] : k,
140
+ prepValue ? prepareForFlechette(valueType!, [x])[0] : x,
141
+ ]);
142
+ }
143
+ return new Map(pairs);
127
144
  });
128
145
  }
129
146
  if (tid === 13) {
@@ -3,7 +3,11 @@
3
3
  // Works with any RpcClient (subprocess or HTTP transport).
4
4
 
5
5
  import { type VgiSchema, schema as schema_, type VgiField, field, type VgiBatch, type VgiDataType, utf8, binary, list } from "../arrow/index.js";
6
- import { type RpcClient, type StreamSession } from "@query-farm/vgi-rpc";
6
+ // `import type`, not `import { type … }`: the latter keeps the statement and
7
+ // emits `import {} from "@query-farm/vgi-rpc"`, a side-effect import of the
8
+ // package root — which re-exports the whole framework, server included, into
9
+ // every browser bundle. See the note in ./errors.
10
+ import type { RpcClient, StreamSession } from "@query-farm/vgi-rpc";
7
11
  import {
8
12
  serializeBindRequest,
9
13
  deserializeBindResponse,
@@ -2,7 +2,16 @@
2
2
  // VgiClient error class + RpcClient wrapper that translates RpcError into
3
3
  // VgiClientError, preserving the worker's remote traceback.
4
4
 
5
- import { RpcError, type RpcClient } from "@query-farm/vgi-rpc";
5
+ // RpcError comes from the `/connect` subpath, not the package root, and the
6
+ // distinction is worth ~160 kB in every browser bundle. The root re-exports the
7
+ // whole framework — protocol, dispatch, access log, the server — and a bundler
8
+ // cannot drop it, so importing one error class from there shipped `RpcServer`
9
+ // to browsers to satisfy an `instanceof`. `RpcClient` is a pure `import type`
10
+ // for the same reason: `import { type X }` leaves the statement behind, and a
11
+ // bare `import {} from "@query-farm/vgi-rpc"` is a side-effect import of that
12
+ // same graph.
13
+ import { RpcError } from "@query-farm/vgi-rpc/connect";
14
+ import type { RpcClient } from "@query-farm/vgi-rpc";
6
15
 
7
16
  /** Error thrown by VgiClient when an RPC call fails or returns unexpected data. */
8
17
  export class VgiClientError extends Error {