@query-farm/vgi 0.24.0 → 0.24.2

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.24.0",
3
+ "version": "0.24.2",
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)",
@@ -69,15 +69,18 @@
69
69
  "files": [
70
70
  "dist",
71
71
  "src",
72
+ "scripts/postinstall.cjs",
72
73
  "!src/**/__tests__/**",
73
74
  "!src/**/*.test.ts",
74
75
  "!src/**/*.spec.ts"
75
76
  ],
77
+ "//imports": "`browser` maps to arrow-js DELIBERATELY, not flechette. Measured on the client surface (VgiClient + Arguments + batch helpers), a minified browser bundle is 84 KB gzip via arrow-js vs 116 KB via flechette — arrow-js tree-shakes to the subset the client touches, while flechette links as a unit. Flechette still wins for `worker-cf`, which exercises far more of the Arrow API and cannot take a peer dep. Keep `browser` AFTER `workerd`/`worker`: conditions match in key order, so a workerd build (which sets both) must hit flechette first.",
76
78
  "imports": {
77
79
  "#arrow-impl": {
78
80
  "flechette": "./src/arrow/impl-flechette/index.ts",
79
81
  "workerd": "./src/arrow/impl-flechette/index.ts",
80
82
  "worker": "./src/arrow/impl-flechette/index.ts",
83
+ "browser": "./src/arrow/impl-arrowjs/index.ts",
81
84
  "default": "./src/arrow/impl-arrowjs/index.ts"
82
85
  }
83
86
  },
@@ -87,12 +90,12 @@
87
90
  },
88
91
  "peerDependencies": {
89
92
  "@query-farm/apache-arrow": "^21.1.1",
90
- "@query-farm/vgi-rpc": "^0.17.0"
93
+ "@query-farm/vgi-rpc": ">=0.18.1 <0.19.0"
91
94
  },
92
95
  "devDependencies": {
93
96
  "@duckdb/node-api": "^1.5.2-r.1",
94
97
  "@query-farm/apache-arrow": "^21.1.1",
95
- "@query-farm/vgi-rpc": "^0.17.0",
98
+ "@query-farm/vgi-rpc": "^0.18.1",
96
99
  "@types/bun": "latest",
97
100
  "typescript": "^6.0.3"
98
101
  },
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // Copyright 2025, 2026 Query Farm LLC - https://query.farm
3
+ // Postinstall: deduplicate @query-farm/apache-arrow across @query-farm/vgi-rpc
4
+ // to avoid dual-package instanceof issues.
5
+ //
6
+ // This is a LOCAL DEVELOPMENT-only fixup for the vgi-typescript monorepo, where
7
+ // @query-farm/vgi-rpc is a `file:` link with its own nested copy of apache-arrow.
8
+ // It must NOT run when this package is installed as a dependency of someone
9
+ // else's project — so it no-ops unless the sibling vgi-rpc-typescript checkout
10
+ // is present next to this repo. (Published consumers get a single hoisted
11
+ // apache-arrow from the registry, so no dedup is needed there.)
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+
15
+ const PKG = "@query-farm/apache-arrow";
16
+
17
+ // Resolve everything relative to the repo root (this file lives in <repo>/scripts),
18
+ // not the process cwd, so behavior doesn't depend on where the installer runs us.
19
+ const repoRoot = path.resolve(__dirname, "..");
20
+
21
+ // Dev-monorepo signal: the sibling vgi-rpc source checkout. When this package is
22
+ // installed as a dependency, that path resolves inside the consumer's node_modules
23
+ // and won't exist — so we bail and leave their tree untouched.
24
+ const devSibling = path.resolve(repoRoot, "..", "vgi-rpc-typescript", "package.json");
25
+ if (!fs.existsSync(devSibling)) {
26
+ process.exit(0);
27
+ }
28
+
29
+ // Deduplicate: replace vgi-rpc's nested copy with a symlink to ours.
30
+ // Without this, two separate DataType classes exist at runtime and
31
+ // instanceof checks fail across package boundaries.
32
+ // Bun uses per-file symlinks for file: deps, so module resolution follows
33
+ // the real path. We must target the REAL node_modules of vgi-rpc-typescript.
34
+ const ourArrow = path.join(repoRoot, "node_modules", PKG);
35
+ const rpcPkgLink = path.join(repoRoot, "node_modules", "@query-farm", "vgi-rpc", "package.json");
36
+ try {
37
+ const realPkgPath = fs.realpathSync(rpcPkgLink);
38
+ const rpcRoot = path.dirname(realPkgPath);
39
+ const rpcArrow = path.join(rpcRoot, "node_modules", PKG);
40
+ const st = fs.lstatSync(rpcArrow);
41
+ if (!st.isSymbolicLink()) {
42
+ fs.rmSync(rpcArrow, { recursive: true });
43
+ fs.symlinkSync(ourArrow, rpcArrow);
44
+ }
45
+ } catch (e) {
46
+ if (e.code !== "ENOENT") throw e;
47
+ }
@@ -17,6 +17,11 @@
17
17
  // default_value: Binary nullable -- serialized single-row RecordBatch with
18
18
  // the default value under "value", or
19
19
  // null when no default is declared
20
+ // required: Bool nullable -- the caller must supply this option at ATTACH
21
+ // time. Nullable and appended LAST so a peer
22
+ // that predates the column reads the batch by
23
+ // name and simply doesn't see it; absent and
24
+ // explicit-null both mean "not required".
20
25
  //
21
26
  // The extension reads the outer `attach_option_specs: list<binary>` column
22
27
  // from CatalogInfo and validates user-supplied ATTACH options against the
@@ -28,6 +33,7 @@ import {
28
33
  field,
29
34
  utf8,
30
35
  binary,
36
+ bool,
31
37
  batchFromColumns,
32
38
  serializeBatch,
33
39
  serializeSchema,
@@ -55,6 +61,16 @@ export interface AttachOptionSpec {
55
61
  * the options dict delivered to attach()).
56
62
  */
57
63
  default?: unknown;
64
+ /**
65
+ * The caller must supply this option at ATTACH time. A catalog that cannot
66
+ * be attached without it advertises that fact at discovery, so a client can
67
+ * say so before attempting the attach rather than surfacing a failure that
68
+ * reads like an empty catalog.
69
+ *
70
+ * Mutually exclusive with `default` — an option that falls back to a value
71
+ * is by definition satisfiable without the caller.
72
+ */
73
+ required?: boolean;
58
74
  }
59
75
 
60
76
  const SPEC_SCHEMA = makeSchema([
@@ -62,6 +78,7 @@ const SPEC_SCHEMA = makeSchema([
62
78
  field("description", utf8(), false),
63
79
  field("type", binary(), false),
64
80
  field("default_value", binary(), true),
81
+ field("required", bool(), true),
65
82
  ]);
66
83
 
67
84
  /**
@@ -69,6 +86,17 @@ const SPEC_SCHEMA = makeSchema([
69
86
  * (one IPC-serialized RecordBatch with a single row).
70
87
  */
71
88
  export function serializeAttachOptionSpec(spec: AttachOptionSpec): Uint8Array {
89
+ // Mirrors AttachOptionSpec.__post_init__ on the Python side: an option that
90
+ // falls back to a value is always satisfiable without the caller, so the
91
+ // combination is a declaration bug rather than a runtime condition.
92
+ if (spec.required && spec.default !== undefined && spec.default !== null) {
93
+ throw new Error(
94
+ `Attach option '${spec.name}' is required but also declares a default ` +
95
+ `(${JSON.stringify(spec.default)}); an option with a default is always ` +
96
+ "satisfiable without the caller. Drop one.",
97
+ );
98
+ }
99
+
72
100
  // `type` is encoded as a serialized Arrow Schema with one field named
73
101
  // "value" of the option's DataType. This lets the extension peek at the
74
102
  // logical type without a separate enum.
@@ -91,6 +119,9 @@ export function serializeAttachOptionSpec(spec: AttachOptionSpec): Uint8Array {
91
119
  description: [spec.description],
92
120
  type: [typeBytes],
93
121
  default_value: [defaultBytes],
122
+ // Written explicitly rather than left null so a reader sees `false`, not
123
+ // NULL, for an option that simply isn't required.
124
+ required: [spec.required ?? false],
94
125
  },
95
126
  SPEC_SCHEMA,
96
127
  );
@@ -105,3 +136,46 @@ export function serializeAttachOptionSpecs(
105
136
  ): Uint8Array[] {
106
137
  return Array.from(specs, serializeAttachOptionSpec);
107
138
  }
139
+
140
+ /**
141
+ * Raised when `attach` omits options declared `required`.
142
+ *
143
+ * Carries the option names in `missing` so a caller can act on them without
144
+ * parsing the message. Mirrors `MissingAttachOptionsError` on the Python side,
145
+ * message included — the extension's integration suite matches on its text.
146
+ */
147
+ export class MissingAttachOptionsError extends Error {
148
+ readonly missing: string[];
149
+
150
+ constructor(catalogName: string, missing: string[]) {
151
+ const joined = missing.map((name) => `'${name}'`).join(", ");
152
+ super(
153
+ `Catalog '${catalogName}' cannot be attached without the required ` +
154
+ `option${missing.length > 1 ? "s" : ""} ${joined}.`,
155
+ );
156
+ this.name = "MissingAttachOptionsError";
157
+ this.missing = missing;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Throw if any `required` spec has no corresponding entry in `options`.
163
+ *
164
+ * Option names are matched case-insensitively, mirroring DuckDB's handling of
165
+ * ATTACH option keys.
166
+ *
167
+ * @throws {MissingAttachOptionsError} If a required option was not supplied.
168
+ */
169
+ export function validateRequiredAttachOptions(
170
+ catalogName: string,
171
+ specs: Iterable<AttachOptionSpec>,
172
+ options: Record<string, unknown>,
173
+ ): void {
174
+ const supplied = new Set(Object.keys(options).map((key) => key.toLowerCase()));
175
+ const missing = Array.from(specs)
176
+ .filter((spec) => spec.required && !supplied.has(spec.name.toLowerCase()))
177
+ .map((spec) => spec.name);
178
+ if (missing.length > 0) {
179
+ throw new MissingAttachOptionsError(catalogName, missing);
180
+ }
181
+ }
@@ -26,6 +26,7 @@ import type { FunctionRegistry } from "../functions/registry.js";
26
26
  import { argumentSpecsToSchema, macroArgumentsSchema } from "../arguments/argument-spec.js";
27
27
  import { serializeSchema, serializeBatch, emptyBatch, batchFromColumns } from "../util/arrow/index.js";
28
28
  import { resolveMetadata } from "../metadata/resolve.js";
29
+ import { type AttachOptionSpec, validateRequiredAttachOptions } from "./attach-option.js";
29
30
  import { FunctionStability, NullHandling, OrderPreservation, DEFAULT_MAX_WORKERS } from "../types.js";
30
31
  import { Arguments } from "../arguments/arguments.js";
31
32
 
@@ -58,6 +59,19 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
58
59
  return [this._descriptor.name];
59
60
  }
60
61
 
62
+ /**
63
+ * Attach-time options this catalog declares, for the named catalog.
64
+ *
65
+ * Empty by default — the declarative descriptor path carries no attach
66
+ * options. A subclass that advertises specs in `catalogsInfo()` should
67
+ * return the same specs here so `attach()` enforces the `required` ones it
68
+ * advertised. Takes the catalog name because one interface may serve
69
+ * several catalogs with different option sets.
70
+ */
71
+ attachOptionSpecs(_name: string): AttachOptionSpec[] {
72
+ return [];
73
+ }
74
+
61
75
  // Advertise the catalog's discovery record. The declarative path carries no
62
76
  // version metadata or attach option specs, but it does surface the optional
63
77
  // `source_url` from the descriptor so it flows through `catalog_catalogs`
@@ -82,6 +96,12 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
82
96
  if (!this.catalogs().includes(name)) {
83
97
  throw new Error(`No worker handles catalog '${name}'`);
84
98
  }
99
+ // Refuse an attach that omits a declared-required option, so the caller
100
+ // gets the option name rather than a catalog that looks mysteriously
101
+ // empty. A subclass that overrides `attach` outright bypasses this and
102
+ // should call `validateRequiredAttachOptions` itself — mirrors
103
+ // CatalogInterface.catalog_attach on the Python side.
104
+ validateRequiredAttachOptions(name, this.attachOptionSpecs(name), options ?? {});
85
105
 
86
106
  const attachOpaqueData = new Uint8Array(16);
87
107
  crypto.getRandomValues(attachOpaqueData);