@query-farm/vgi 0.32.0 → 0.34.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.32.0",
3
+ "version": "0.34.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.24.0 <0.25.0"
94
+ "@query-farm/vgi-rpc": ">=0.25.2 <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.24.0",
99
+ "@query-farm/vgi-rpc": "^0.25.2",
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) {
@@ -16,7 +16,18 @@ import type { VgiBatch } from "../types.js";
16
16
  */
17
17
  export function withBatchMetadata(batch: VgiBatch, metadata: Map<string, string>): VgiBatch {
18
18
  const t = batch as any;
19
- const clone = Object.assign(Object.create(Object.getPrototypeOf(t)), t);
19
+ // Copy the own properties as descriptors, not by assignment. vgi-rpc's reader
20
+ // pins a zero-column batch's row count as an own `numRows` over
21
+ // `Table.prototype.numRows`, a getter-only accessor, and `Object.assign`
22
+ // *assigns* each own property -- which throws in strict mode ("Attempted to
23
+ // assign to readonly property") over an inherited getter-only accessor.
24
+ const clone = Object.create(Object.getPrototypeOf(t), Object.getOwnPropertyDescriptors(t));
25
+ // This backend's own reader supplies that row count through a Proxy instead
26
+ // (see `deserializeBatch`), which no property copy carries: without this the
27
+ // clone would derive 0 rows from its absent columns.
28
+ if (clone.numRows !== t.numRows) {
29
+ Object.defineProperty(clone, "numRows", { value: t.numRows, configurable: true, enumerable: true });
30
+ }
20
31
  if (metadata && metadata.size > 0) {
21
32
  clone._vgiRecordMetadata = metadata;
22
33
  clone.metadata = metadata;
@@ -18,7 +18,7 @@
18
18
  import {
19
19
  Type,
20
20
  field as f_field,
21
- nullType, bool, int, float, utf8, binary, fixedSizeBinary,
21
+ nullType, bool, int, float, utf8, largeUtf8, binary, largeBinary, fixedSizeBinary,
22
22
  decimal, date, time, timestamp, duration, interval,
23
23
  list, largeList, fixedSizeList, struct, map, dictionary, union,
24
24
  } from "@query-farm/flechette";
@@ -47,12 +47,19 @@ export function toFlechetteType(type: any): any {
47
47
  return aliasIntSigned(int(type.bitWidth, type.isSigned ?? type.signed ?? true));
48
48
  case Type.Float:
49
49
  return float(type.precision);
50
+ // Large variants stay large: flechette builds and encodes them, and
51
+ // folding them into Binary/Utf8 put a different type on the wire than the
52
+ // one declared (arrow-js writes LargeBinary/LargeUtf8). InitRequest
53
+ // declares `pushdown_filters` large_binary, so a flechette worker or
54
+ // client wrote that field as binary.
50
55
  case Type.Binary:
51
- case Type.LargeBinary:
52
56
  return binary();
57
+ case Type.LargeBinary:
58
+ return largeBinary();
53
59
  case Type.Utf8:
54
- case Type.LargeUtf8:
55
60
  return utf8();
61
+ case Type.LargeUtf8:
62
+ return largeUtf8();
56
63
  case Type.FixedSizeBinary:
57
64
  return fixedSizeBinary(type.byteWidth ?? type.stride);
58
65
  case Type.Decimal: