@query-farm/vgi 0.24.1 → 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.1",
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)",
@@ -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);