@query-farm/vgi 0.9.0 → 0.10.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.9.0",
3
+ "version": "0.10.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)",
@@ -19,6 +19,7 @@ import {
19
19
  VGI_RANGE_KEY,
20
20
  VGI_PATTERN_KEY,
21
21
  } from "../types.js";
22
+ import { ArgumentValidationError } from "../errors.js";
22
23
 
23
24
  export interface ArgumentSpec {
24
25
  name: string;
@@ -151,6 +152,73 @@ export function constraintSpecFields(
151
152
  return fields;
152
153
  }
153
154
 
155
+ /** Coerce a numeric const value (number or int64 `bigint`) to `number`. */
156
+ function toNumber(value: unknown): number | undefined {
157
+ if (typeof value === "number") return value;
158
+ if (typeof value === "bigint") return Number(value);
159
+ return undefined;
160
+ }
161
+
162
+ /** Equality for a choice comparison, coercing numeric/bigint operands. */
163
+ function choiceEquals(choice: unknown, value: unknown): boolean {
164
+ const cn = toNumber(choice);
165
+ const vn = toNumber(value);
166
+ if (cn !== undefined && vn !== undefined) return cn === vn;
167
+ return choice === value;
168
+ }
169
+
170
+ /**
171
+ * Enforce a bind-time const value against its declared {@link ArgumentConstraints},
172
+ * throwing {@link ArgumentValidationError} on violation. Mirrors the Python SDK's
173
+ * `Arg._validate`: numeric range (`ge`/`le`/`gt`/`lt`), closed choice set, and
174
+ * regex `pattern`. A `null`/`undefined` value skips its value constraints.
175
+ */
176
+ export function validateConstConstraints(
177
+ name: string,
178
+ constraints: ArgumentConstraints,
179
+ value: unknown,
180
+ ): void {
181
+ if (value === null || value === undefined) return;
182
+
183
+ const num = toNumber(value);
184
+ if (num !== undefined) {
185
+ if (constraints.ge !== undefined && num < constraints.ge) {
186
+ throw new ArgumentValidationError(`argument '${name}' must be >= ${constraints.ge}`);
187
+ }
188
+ if (constraints.le !== undefined && num > constraints.le) {
189
+ throw new ArgumentValidationError(`argument '${name}' must be <= ${constraints.le}`);
190
+ }
191
+ if (constraints.gt !== undefined && num <= constraints.gt) {
192
+ throw new ArgumentValidationError(`argument '${name}' must be > ${constraints.gt}`);
193
+ }
194
+ if (constraints.lt !== undefined && num >= constraints.lt) {
195
+ throw new ArgumentValidationError(`argument '${name}' must be < ${constraints.lt}`);
196
+ }
197
+ }
198
+
199
+ if (constraints.choices !== undefined && constraints.choices.length > 0) {
200
+ if (!constraints.choices.some((c) => choiceEquals(c, value))) {
201
+ throw new ArgumentValidationError(
202
+ `argument '${name}' must be one of ${JSON.stringify([...constraints.choices])}`,
203
+ );
204
+ }
205
+ }
206
+
207
+ if (constraints.pattern !== undefined && typeof value === "string") {
208
+ let re: RegExp | undefined;
209
+ try {
210
+ re = new RegExp(constraints.pattern);
211
+ } catch {
212
+ re = undefined; // an invalid declared regex must not reject the caller
213
+ }
214
+ if (re !== undefined && !re.test(value)) {
215
+ throw new ArgumentValidationError(
216
+ `argument '${name}' must match pattern ${JSON.stringify(constraints.pattern)}`,
217
+ );
218
+ }
219
+ }
220
+ }
221
+
154
222
  function argumentSpecSortKey(spec: ArgumentSpec): [number, number | string] {
155
223
  if (typeof spec.position === "number") {
156
224
  return [0, spec.position];
@@ -37,7 +37,12 @@ import type {
37
37
  FunctionExample,
38
38
  } from "./types.js";
39
39
  import type { ArgumentSpec } from "../arguments/argument-spec.js";
40
- import { argumentSpecsToSchema, constraintSpecFields } from "../arguments/argument-spec.js";
40
+ import {
41
+ argumentSpecsToSchema,
42
+ constraintSpecFields,
43
+ validateConstConstraints,
44
+ type ArgumentConstraints,
45
+ } from "../arguments/argument-spec.js";
41
46
  import { batchToScalarDict, batchToSecretDict, batchFromColumns, projectBatch, safeNumber } from "../util/arrow/index.js";
42
47
 
43
48
  // ============================================================================
@@ -232,6 +237,26 @@ export function defineScalarFunction<
232
237
  }
233
238
  }
234
239
 
240
+ // Raw const-arg constraints for bind-time enforcement, keyed by arg name.
241
+ // Only the ordered `parameters` API carries constraints (the legacy
242
+ // params/constParams path does not), so only its const params appear here.
243
+ const constConstraints = new Map<string, ArgumentConstraints>();
244
+ if (config.parameters) {
245
+ for (const p of config.parameters) {
246
+ if (
247
+ p.const &&
248
+ (p.choices !== undefined ||
249
+ p.ge !== undefined ||
250
+ p.le !== undefined ||
251
+ p.gt !== undefined ||
252
+ p.lt !== undefined ||
253
+ p.pattern !== undefined)
254
+ ) {
255
+ constConstraints.set(p.name, p);
256
+ }
257
+ }
258
+ }
259
+
235
260
  const meta: FunctionMeta = {
236
261
  name: config.name,
237
262
  description: config.description,
@@ -270,7 +295,11 @@ export function defineScalarFunction<
270
295
  let constIdx = 0;
271
296
  for (const spec of specs) {
272
297
  if (spec.isConst) {
273
- constArgs[spec.name] = request.arguments.get(constIdx, undefined);
298
+ const value = request.arguments.get(constIdx, undefined);
299
+ constArgs[spec.name] = value;
300
+ // Enforce declared const-arg constraints at bind (choices/range/pattern).
301
+ const constraints = constConstraints.get(spec.name);
302
+ if (constraints) validateConstConstraints(spec.name, constraints, value);
274
303
  constIdx++;
275
304
  }
276
305
  }