@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/dist/catalog/attach-option.d.ts +30 -0
- package/dist/catalog/read-only.d.ts +11 -0
- package/dist/index.js +55 -28
- package/dist/index.js.map +5 -5
- package/dist/worker-cf-entry.js +55 -28
- package/dist/worker-cf-entry.js.map +5 -5
- package/package.json +6 -3
- package/scripts/postinstall.cjs +47 -0
- package/src/catalog/attach-option.ts +74 -0
- package/src/catalog/read-only.ts +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@query-farm/vgi",
|
|
3
|
-
"version": "0.24.
|
|
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": "
|
|
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.
|
|
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
|
+
}
|
package/src/catalog/read-only.ts
CHANGED
|
@@ -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);
|