@query-farm/vgi 0.8.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/dist/arguments/argument-spec.d.ts +56 -0
- package/dist/functions/scalar.d.ts +14 -0
- package/dist/index.core.d.ts +1 -1
- package/dist/index.js +245 -105
- package/dist/index.js.map +7 -7
- package/dist/types.d.ts +4 -0
- package/dist/worker-cf-entry.js +208 -69
- package/dist/worker-cf-entry.js.map +6 -6
- package/package.json +1 -1
- package/src/arguments/argument-spec.ts +216 -0
- package/src/functions/scalar.ts +48 -2
- package/src/index.core.ts +1 -1
- package/src/types.ts +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@query-farm/vgi",
|
|
3
|
-
"version": "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)",
|
|
@@ -14,7 +14,12 @@ import {
|
|
|
14
14
|
VGI_CONST_KEY,
|
|
15
15
|
VGI_CONST_TRUE,
|
|
16
16
|
VGI_DOC_KEY,
|
|
17
|
+
VGI_DEFAULT_KEY,
|
|
18
|
+
VGI_CHOICES_KEY,
|
|
19
|
+
VGI_RANGE_KEY,
|
|
20
|
+
VGI_PATTERN_KEY,
|
|
17
21
|
} from "../types.js";
|
|
22
|
+
import { ArgumentValidationError } from "../errors.js";
|
|
18
23
|
|
|
19
24
|
export interface ArgumentSpec {
|
|
20
25
|
name: string;
|
|
@@ -25,6 +30,193 @@ export interface ArgumentSpec {
|
|
|
25
30
|
isVarargs?: boolean;
|
|
26
31
|
isConst?: boolean;
|
|
27
32
|
doc?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Discovery-facing constraint metadata, pre-encoded to their surfaced UTF-8
|
|
35
|
+
* forms (presence-only — `undefined` means the constraint is absent and the
|
|
36
|
+
* corresponding field-metadata key is omitted entirely). Mirrors Python's
|
|
37
|
+
* `ArgumentSpec.default_json` / `choices_json` / `range_notation` / `pattern`
|
|
38
|
+
* so `argumentSpecsToSchema`/`schemaToArgumentSpecs` round-trip symmetrically.
|
|
39
|
+
*/
|
|
40
|
+
defaultJson?: string;
|
|
41
|
+
choicesJson?: string;
|
|
42
|
+
rangeNotation?: string;
|
|
43
|
+
pattern?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Raw per-argument constraint declarations, as authored on a function's
|
|
48
|
+
* argument descriptor. Encoded into the pre-computed {@link ArgumentSpec}
|
|
49
|
+
* constraint fields via {@link constraintSpecFields}.
|
|
50
|
+
*/
|
|
51
|
+
export interface ArgumentConstraints {
|
|
52
|
+
/** Closed set of allowed values (JSON-encoded into `vgi_choices`). */
|
|
53
|
+
choices?: readonly unknown[];
|
|
54
|
+
/** Inclusive lower bound (`>=`). */
|
|
55
|
+
ge?: number;
|
|
56
|
+
/** Inclusive upper bound (`<=`). */
|
|
57
|
+
le?: number;
|
|
58
|
+
/** Exclusive lower bound (`>`). */
|
|
59
|
+
gt?: number;
|
|
60
|
+
/** Exclusive upper bound (`<`). */
|
|
61
|
+
lt?: number;
|
|
62
|
+
/** Regex the value must match (surfaced as `vgi_pattern`, as-is). */
|
|
63
|
+
pattern?: string;
|
|
64
|
+
/** Default value (JSON-encoded into `vgi_default`); optional args only. */
|
|
65
|
+
default?: unknown;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** `JSON.stringify`, falling back to the value's string form when unserializable. */
|
|
69
|
+
function safeJson(value: unknown): string {
|
|
70
|
+
try {
|
|
71
|
+
const encoded = JSON.stringify(value);
|
|
72
|
+
// JSON.stringify returns undefined for functions/symbols/undefined.
|
|
73
|
+
if (encoded === undefined) {
|
|
74
|
+
return JSON.stringify(String(value));
|
|
75
|
+
}
|
|
76
|
+
return encoded;
|
|
77
|
+
} catch {
|
|
78
|
+
return JSON.stringify(String(value));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build interval notation from an argument's numeric bounds.
|
|
84
|
+
*
|
|
85
|
+
* Inclusive bounds (`ge`/`le`) render as square brackets, exclusive bounds
|
|
86
|
+
* (`gt`/`lt`) as parentheses, and an open side as `-inf`/`+inf`. Returns
|
|
87
|
+
* `undefined` when the argument has no numeric bound at all.
|
|
88
|
+
*
|
|
89
|
+
* Examples: `ge=0,le=10` -> `"[0, 10]"`; `gt=0` -> `"(0, +inf)"`;
|
|
90
|
+
* `ge=1,lt=10` -> `"[1, 10)"`.
|
|
91
|
+
*/
|
|
92
|
+
export function formatRange(
|
|
93
|
+
ge?: number,
|
|
94
|
+
le?: number,
|
|
95
|
+
gt?: number,
|
|
96
|
+
lt?: number,
|
|
97
|
+
): string | undefined {
|
|
98
|
+
if (ge === undefined && le === undefined && gt === undefined && lt === undefined) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
let low: string;
|
|
102
|
+
if (gt !== undefined) {
|
|
103
|
+
low = `(${gt}`;
|
|
104
|
+
} else if (ge !== undefined) {
|
|
105
|
+
low = `[${ge}`;
|
|
106
|
+
} else {
|
|
107
|
+
low = "(-inf";
|
|
108
|
+
}
|
|
109
|
+
let high: string;
|
|
110
|
+
if (lt !== undefined) {
|
|
111
|
+
high = `${lt})`;
|
|
112
|
+
} else if (le !== undefined) {
|
|
113
|
+
high = `${le}]`;
|
|
114
|
+
} else {
|
|
115
|
+
high = "+inf)";
|
|
116
|
+
}
|
|
117
|
+
return `${low}, ${high}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Encode raw {@link ArgumentConstraints} into the pre-computed constraint
|
|
122
|
+
* fields of an {@link ArgumentSpec} (presence-only — absent constraints yield
|
|
123
|
+
* no field). Mirrors Python's `_constraint_kwargs`.
|
|
124
|
+
*/
|
|
125
|
+
export function constraintSpecFields(
|
|
126
|
+
constraints: ArgumentConstraints | undefined,
|
|
127
|
+
): Pick<ArgumentSpec, "defaultJson" | "choicesJson" | "rangeNotation" | "pattern"> {
|
|
128
|
+
const fields: Pick<
|
|
129
|
+
ArgumentSpec,
|
|
130
|
+
"defaultJson" | "choicesJson" | "rangeNotation" | "pattern"
|
|
131
|
+
> = {};
|
|
132
|
+
if (constraints === undefined) return fields;
|
|
133
|
+
|
|
134
|
+
if ("default" in constraints && constraints.default !== undefined) {
|
|
135
|
+
fields.defaultJson = safeJson(constraints.default);
|
|
136
|
+
}
|
|
137
|
+
if (constraints.choices !== undefined) {
|
|
138
|
+
fields.choicesJson = safeJson([...constraints.choices]);
|
|
139
|
+
}
|
|
140
|
+
const range = formatRange(
|
|
141
|
+
constraints.ge,
|
|
142
|
+
constraints.le,
|
|
143
|
+
constraints.gt,
|
|
144
|
+
constraints.lt,
|
|
145
|
+
);
|
|
146
|
+
if (range !== undefined) {
|
|
147
|
+
fields.rangeNotation = range;
|
|
148
|
+
}
|
|
149
|
+
if (constraints.pattern !== undefined) {
|
|
150
|
+
fields.pattern = constraints.pattern;
|
|
151
|
+
}
|
|
152
|
+
return fields;
|
|
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
|
+
}
|
|
28
220
|
}
|
|
29
221
|
|
|
30
222
|
function argumentSpecSortKey(spec: ArgumentSpec): [number, number | string] {
|
|
@@ -76,6 +268,20 @@ export function argumentSpecsToSchema(specs: ArgumentSpec[]): VgiSchema {
|
|
|
76
268
|
metadata.set(VGI_DOC_KEY, spec.doc);
|
|
77
269
|
}
|
|
78
270
|
|
|
271
|
+
// Per-argument constraint metadata (presence-only; already value-encoded).
|
|
272
|
+
if (spec.defaultJson !== undefined) {
|
|
273
|
+
metadata.set(VGI_DEFAULT_KEY, spec.defaultJson);
|
|
274
|
+
}
|
|
275
|
+
if (spec.choicesJson !== undefined) {
|
|
276
|
+
metadata.set(VGI_CHOICES_KEY, spec.choicesJson);
|
|
277
|
+
}
|
|
278
|
+
if (spec.rangeNotation !== undefined) {
|
|
279
|
+
metadata.set(VGI_RANGE_KEY, spec.rangeNotation);
|
|
280
|
+
}
|
|
281
|
+
if (spec.pattern !== undefined) {
|
|
282
|
+
metadata.set(VGI_PATTERN_KEY, spec.pattern);
|
|
283
|
+
}
|
|
284
|
+
|
|
79
285
|
const field = field_(
|
|
80
286
|
spec.name,
|
|
81
287
|
spec.arrowType,
|
|
@@ -184,6 +390,12 @@ export function schemaToArgumentSpecs(schema: VgiSchema): ArgumentSpec[] {
|
|
|
184
390
|
const isConst = metadata.get(VGI_CONST_KEY) === VGI_CONST_TRUE;
|
|
185
391
|
const doc = metadata.get(VGI_DOC_KEY) ?? "";
|
|
186
392
|
|
|
393
|
+
// Per-argument constraint metadata (absent key -> undefined).
|
|
394
|
+
const defaultJson = metadata.get(VGI_DEFAULT_KEY);
|
|
395
|
+
const choicesJson = metadata.get(VGI_CHOICES_KEY);
|
|
396
|
+
const rangeNotation = metadata.get(VGI_RANGE_KEY);
|
|
397
|
+
const pattern = metadata.get(VGI_PATTERN_KEY);
|
|
398
|
+
|
|
187
399
|
specs.push({
|
|
188
400
|
name: field.name,
|
|
189
401
|
position,
|
|
@@ -193,6 +405,10 @@ export function schemaToArgumentSpecs(schema: VgiSchema): ArgumentSpec[] {
|
|
|
193
405
|
isVarargs,
|
|
194
406
|
isConst,
|
|
195
407
|
doc,
|
|
408
|
+
defaultJson,
|
|
409
|
+
choicesJson,
|
|
410
|
+
rangeNotation,
|
|
411
|
+
pattern,
|
|
196
412
|
});
|
|
197
413
|
}
|
|
198
414
|
|
package/src/functions/scalar.ts
CHANGED
|
@@ -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 {
|
|
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
|
// ============================================================================
|
|
@@ -71,6 +76,22 @@ export interface ScalarParameterDef {
|
|
|
71
76
|
varargs?: boolean;
|
|
72
77
|
/** Human-readable per-argument description (surfaced as `vgi_doc`). */
|
|
73
78
|
doc?: string;
|
|
79
|
+
// Discovery-facing validation constraints (surfaced via
|
|
80
|
+
// `vgi_function_arguments()`). All optional; see `ArgumentConstraints`.
|
|
81
|
+
/** Closed set of allowed values (surfaced as `vgi_choices`). */
|
|
82
|
+
choices?: readonly unknown[];
|
|
83
|
+
/** Inclusive lower bound, `>=` (folded into `vgi_range`). */
|
|
84
|
+
ge?: number;
|
|
85
|
+
/** Inclusive upper bound, `<=` (folded into `vgi_range`). */
|
|
86
|
+
le?: number;
|
|
87
|
+
/** Exclusive lower bound, `>` (folded into `vgi_range`). */
|
|
88
|
+
gt?: number;
|
|
89
|
+
/** Exclusive upper bound, `<` (folded into `vgi_range`). */
|
|
90
|
+
lt?: number;
|
|
91
|
+
/** Regex the value must match (surfaced as `vgi_pattern`). */
|
|
92
|
+
pattern?: string;
|
|
93
|
+
/** Default value (surfaced as `vgi_default`, JSON-encoded). */
|
|
94
|
+
default?: unknown;
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
/**
|
|
@@ -183,6 +204,7 @@ export function defineScalarFunction<
|
|
|
183
204
|
isConst: p.const,
|
|
184
205
|
isVarargs: p.varargs,
|
|
185
206
|
doc: p.doc,
|
|
207
|
+
...constraintSpecFields(p),
|
|
186
208
|
});
|
|
187
209
|
}
|
|
188
210
|
} else {
|
|
@@ -215,6 +237,26 @@ export function defineScalarFunction<
|
|
|
215
237
|
}
|
|
216
238
|
}
|
|
217
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
|
+
|
|
218
260
|
const meta: FunctionMeta = {
|
|
219
261
|
name: config.name,
|
|
220
262
|
description: config.description,
|
|
@@ -253,7 +295,11 @@ export function defineScalarFunction<
|
|
|
253
295
|
let constIdx = 0;
|
|
254
296
|
for (const spec of specs) {
|
|
255
297
|
if (spec.isConst) {
|
|
256
|
-
|
|
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);
|
|
257
303
|
constIdx++;
|
|
258
304
|
}
|
|
259
305
|
}
|
package/src/index.core.ts
CHANGED
|
@@ -75,7 +75,7 @@ export type {
|
|
|
75
75
|
|
|
76
76
|
// Arguments
|
|
77
77
|
export { Arguments } from "./arguments/arguments.js";
|
|
78
|
-
export { type ArgumentSpec, argumentSpecsToSchema, schemaToArgumentSpecs, macroArgumentsSchema, macroParameterDocsFromSchema } from "./arguments/argument-spec.js";
|
|
78
|
+
export { type ArgumentSpec, type ArgumentConstraints, argumentSpecsToSchema, schemaToArgumentSpecs, macroArgumentsSchema, macroParameterDocsFromSchema, formatRange, constraintSpecFields } from "./arguments/argument-spec.js";
|
|
79
79
|
|
|
80
80
|
// Errors
|
|
81
81
|
export {
|
package/src/types.ts
CHANGED
|
@@ -62,6 +62,20 @@ export const VGI_CONST_TRUE = "true";
|
|
|
62
62
|
// Per-argument description (UTF-8; presence-only). `vgi_doc_*` prefix reserved.
|
|
63
63
|
export const VGI_DOC_KEY = "vgi_doc";
|
|
64
64
|
|
|
65
|
+
// Per-argument constraint metadata for agent discovery. All presence-only
|
|
66
|
+
// (omitted when absent) and value-encoded as UTF-8 so the surfaced column type
|
|
67
|
+
// stays uniform regardless of the arg's value type:
|
|
68
|
+
// vgi_default — JSON scalar (the arg's default value; optional args only)
|
|
69
|
+
// vgi_choices — JSON array (the closed set of allowed values)
|
|
70
|
+
// vgi_range — interval notation built from ge/le/gt/lt (e.g. "[0, 100]",
|
|
71
|
+
// "(0, +inf)", "[1, 10)"); a discovery surface, not raw bounds
|
|
72
|
+
// vgi_pattern — raw regex the value must match (open set)
|
|
73
|
+
// Kept byte-for-byte in sync with the C++ reader and the Python/other SDKs.
|
|
74
|
+
export const VGI_DEFAULT_KEY = "vgi_default";
|
|
75
|
+
export const VGI_CHOICES_KEY = "vgi_choices";
|
|
76
|
+
export const VGI_RANGE_KEY = "vgi_range";
|
|
77
|
+
export const VGI_PATTERN_KEY = "vgi_pattern";
|
|
78
|
+
|
|
65
79
|
export interface TableCardinality {
|
|
66
80
|
estimate: number | null;
|
|
67
81
|
max: number | null;
|