@query-farm/vgi 0.27.0 → 0.29.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/README.md +26 -7
- package/dist/arguments/argument-spec.d.ts +53 -0
- package/dist/arrow/impl-arrowjs/index.d.ts +1 -1
- package/dist/arrow/impl-arrowjs/iterate.d.ts +10 -1
- package/dist/arrow/impl-arrowjs/schema.d.ts +2 -0
- package/dist/arrow/impl-flechette/index.d.ts +1 -1
- package/dist/arrow/impl-flechette/iterate.d.ts +10 -1
- package/dist/arrow/impl-flechette/schema.d.ts +2 -0
- package/dist/arrow/index.d.ts +1 -1
- package/dist/arrow/schema-types.d.ts +3 -1
- package/dist/arrow/types.d.ts +39 -3
- package/dist/catalog/interface.d.ts +43 -0
- package/dist/client/client.d.ts +1 -1
- package/dist/client-entry.js +360 -178
- package/dist/client-entry.js.map +14 -14
- package/dist/functions/table.d.ts +63 -0
- package/dist/functions/types.d.ts +38 -0
- package/dist/generated/vgi-client.d.ts +59 -0
- package/dist/generated/vgi-protocol-schemas.d.ts +19 -0
- package/dist/http/fetch.d.ts +10 -0
- package/dist/index.core.d.ts +2 -2
- package/dist/index.js +1203 -834
- package/dist/index.js.map +40 -37
- package/dist/protocol/handlers/catalog/shared.d.ts +0 -8
- package/dist/protocol/handlers/function.d.ts +11 -0
- package/dist/protocol/serializers/bind.d.ts +1 -2
- package/dist/protocol/serializers/cardinality.d.ts +1 -2
- package/dist/protocol/serializers/splits.d.ts +126 -0
- package/dist/protocol/types.d.ts +27 -0
- package/dist/request-auth.d.ts +28 -0
- package/dist/serve-entry.d.ts +4 -0
- package/dist/serve-entry.js +745 -359
- package/dist/serve-entry.js.map +32 -29
- package/dist/split-token.d.ts +88 -0
- package/dist/worker-cf-entry.js +1029 -619
- package/dist/worker-cf-entry.js.map +37 -34
- package/package.json +3 -3
- package/src/arguments/argument-spec.ts +81 -0
- package/src/arguments/arguments.ts +0 -1
- package/src/arrow/impl-arrowjs/index.ts +1 -1
- package/src/arrow/impl-arrowjs/iterate.ts +21 -0
- package/src/arrow/impl-arrowjs/schema.ts +4 -0
- package/src/arrow/impl-flechette/index.ts +1 -1
- package/src/arrow/impl-flechette/iterate.ts +24 -1
- package/src/arrow/impl-flechette/schema.ts +4 -0
- package/src/arrow/index.ts +1 -1
- package/src/arrow/schema-types.ts +4 -0
- package/src/arrow/types.ts +39 -3
- package/src/catalog/interface.ts +124 -29
- package/src/catalog/read-only.ts +18 -18
- package/src/client/client.ts +6 -2
- package/src/functions/aggregate.ts +3 -0
- package/src/functions/copy-from.ts +2 -1
- package/src/functions/copy-to.ts +2 -1
- package/src/functions/scalar.ts +11 -0
- package/src/functions/table-buffering.ts +3 -0
- package/src/functions/table-in-out.ts +8 -4
- package/src/functions/table.ts +129 -2
- package/src/functions/types.ts +40 -0
- package/src/generated/vgi-client.ts +63 -1
- package/src/generated/vgi-protocol-schemas.ts +240 -14
- package/src/http/fetch.ts +117 -2
- package/src/index.core.ts +2 -2
- package/src/protocol/handlers/aggregate.ts +10 -5
- package/src/protocol/handlers/catalog/admin.ts +41 -48
- package/src/protocol/handlers/catalog/index_methods.ts +4 -11
- package/src/protocol/handlers/catalog/macro.ts +8 -16
- package/src/protocol/handlers/catalog/shared.ts +10 -57
- package/src/protocol/handlers/catalog/table.ts +33 -96
- package/src/protocol/handlers/catalog/view.ts +13 -17
- package/src/protocol/handlers/function.ts +311 -18
- package/src/protocol/handlers/table-buffering.ts +7 -4
- package/src/protocol/serializers/bind.ts +56 -63
- package/src/protocol/serializers/cardinality.ts +7 -7
- package/src/protocol/serializers/init.ts +52 -10
- package/src/protocol/serializers/splits.ts +219 -0
- package/src/protocol/state-serializer.ts +55 -1
- package/src/protocol/types.ts +27 -0
- package/src/request-auth.ts +73 -0
- package/src/serve-entry.ts +5 -0
- package/src/split-token.ts +297 -0
- package/src/worker-cf-entry.ts +2 -1
- package/src/worker.ts +30 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@query-farm/vgi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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)",
|
|
@@ -90,12 +90,12 @@
|
|
|
90
90
|
},
|
|
91
91
|
"peerDependencies": {
|
|
92
92
|
"@query-farm/apache-arrow": "^21.1.1",
|
|
93
|
-
"@query-farm/vgi-rpc": ">=0.
|
|
93
|
+
"@query-farm/vgi-rpc": ">=0.22.0 <0.23.0"
|
|
94
94
|
},
|
|
95
95
|
"devDependencies": {
|
|
96
96
|
"@duckdb/node-api": "^1.5.5-r.4",
|
|
97
97
|
"@query-farm/apache-arrow": "^21.1.1",
|
|
98
|
-
"@query-farm/vgi-rpc": "^0.
|
|
98
|
+
"@query-farm/vgi-rpc": "^0.22.0",
|
|
99
99
|
"@types/bun": "latest",
|
|
100
100
|
"typescript": "^7.0.2"
|
|
101
101
|
},
|
|
@@ -167,6 +167,87 @@ function choiceEquals(choice: unknown, value: unknown): boolean {
|
|
|
167
167
|
return choice === value;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Narrow a bind-time argument value from Arrow's bigint to a plain number.
|
|
172
|
+
*
|
|
173
|
+
* DuckDB types an integer literal as BIGINT, so `series(3)` arrives as `3n`
|
|
174
|
+
* whether or not anyone wanted 64 bits. Nobody writes `BigInt(1000)` for a page
|
|
175
|
+
* size, so the arg paths narrow — and they now all narrow the same way, which
|
|
176
|
+
* they did not before (two sites used safeNumber, two a bare Number).
|
|
177
|
+
*
|
|
178
|
+
* This is lossy above 2^53, and that is a deliberate call rather than an
|
|
179
|
+
* oversight. Every int64 *argument* across the fixtures and every shipped
|
|
180
|
+
* worker is a small control value — count, page_size, batch_size, increment,
|
|
181
|
+
* lag_minutes, restatement_lookback_days. The large int64s in the test suite
|
|
182
|
+
* are all somewhere else entirely:
|
|
183
|
+
*
|
|
184
|
+
* - column data (copy_to/types.test, copy_to/parallel.test) — never touches
|
|
185
|
+
* this path;
|
|
186
|
+
* - filter-pushdown constants (filter_pushdown/integers.test pins 2^53+1 on
|
|
187
|
+
* purpose) — deserialize.ts keeps those bigint, with its own regression
|
|
188
|
+
* test;
|
|
189
|
+
* - varargs values (constant_columns) — read raw off bindCall.arguments,
|
|
190
|
+
* because extractArgs' copy of them is not what the function reads;
|
|
191
|
+
* - time-travel versions — carried as strings (`at_value`).
|
|
192
|
+
*
|
|
193
|
+
* Preserving the bigint on overflow was tried and reverted: it buys nothing for
|
|
194
|
+
* any argument that exists, and costs a JS type that depends on magnitude, so a
|
|
195
|
+
* worker doing `args.n + 1` would work in every test and fail on one caller's
|
|
196
|
+
* input. If an argument ever genuinely needs the full range — a snowflake id, a
|
|
197
|
+
* nanosecond epoch — the fix is to make int64 arguments bigint by declared
|
|
198
|
+
* type, not by magnitude.
|
|
199
|
+
*/
|
|
200
|
+
export function narrowArgValue(value: unknown): unknown {
|
|
201
|
+
return typeof value === "bigint" ? Number(value) : value;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Reject a type *factory* where an Arrow *type* was expected.
|
|
206
|
+
*
|
|
207
|
+
* `params: { n: int64 }` (no parentheses) is accepted by the type system —
|
|
208
|
+
* `VgiDataType` is loose enough — and then silently produces an ArgumentSpec
|
|
209
|
+
* whose `arrowType` is a Function. Nothing complains until the schema is
|
|
210
|
+
* serialized or the bind arrives, and the error at that point names neither the
|
|
211
|
+
* argument nor the cause.
|
|
212
|
+
*
|
|
213
|
+
* The mistake is easy to make on purpose, too: the same identifier is a
|
|
214
|
+
* ready-made type on one entry point and a factory on another.
|
|
215
|
+
*
|
|
216
|
+
* @query-farm/vgi int, int32, float32, bool → type instances
|
|
217
|
+
* @query-farm/vgi/worker-cf int, int32, float32, bool → factories
|
|
218
|
+
*
|
|
219
|
+
* So moving a worker to Cloudflare can turn a correct declaration into a
|
|
220
|
+
* silently wrong one with no other edit. Failing here, at definition time,
|
|
221
|
+
* makes that a startup error naming the field.
|
|
222
|
+
*/
|
|
223
|
+
export function assertArrowType(value: unknown, where: string): void {
|
|
224
|
+
if (typeof value === "function") {
|
|
225
|
+
const name = (value as { name?: string }).name;
|
|
226
|
+
const call = name ? `${name}()` : "the factory";
|
|
227
|
+
throw new ArgumentValidationError(
|
|
228
|
+
`${where} is a type factory, not an Arrow type — call it: ${call}. ` +
|
|
229
|
+
`(Note that int, int32, float32 and bool are ready-made types on ` +
|
|
230
|
+
`@query-farm/vgi but factories on @query-farm/vgi/worker-cf, so the ` +
|
|
231
|
+
`same name may need parentheses on one entry point and not the other. ` +
|
|
232
|
+
`int64(), float64(), utf8() and the rest are factories everywhere.)`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (value === null || value === undefined) {
|
|
236
|
+
throw new ArgumentValidationError(`${where} is ${String(value)}, not an Arrow type.`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** {@link assertArrowType} over a record of declared types, e.g. `params`. */
|
|
241
|
+
export function assertArrowTypes(
|
|
242
|
+
types: Record<string, unknown> | undefined,
|
|
243
|
+
where: string,
|
|
244
|
+
): void {
|
|
245
|
+
if (!types) return;
|
|
246
|
+
for (const [name, type] of Object.entries(types)) {
|
|
247
|
+
assertArrowType(type, `${where}.${name}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
170
251
|
/**
|
|
171
252
|
* Enforce a bind-time const value against its declared {@link ArgumentConstraints},
|
|
172
253
|
* throwing {@link ArgumentValidationError} on violation. Mirrors the Python SDK's
|
|
@@ -17,7 +17,7 @@ export {
|
|
|
17
17
|
int8, int16, int32, int64,
|
|
18
18
|
uint8, uint16, uint32, uint64,
|
|
19
19
|
float16, float32, float64,
|
|
20
|
-
utf8, binary, fixedSizeBinary,
|
|
20
|
+
utf8, largeUtf8, binary, largeBinary, fixedSizeBinary,
|
|
21
21
|
decimal, decimal128, decimal256,
|
|
22
22
|
date, dateDay, dateMillisecond,
|
|
23
23
|
time, timeSecond, timeMillisecond, timeMicrosecond, timeNanosecond,
|
|
@@ -151,8 +151,29 @@ export function batchToSecretDict(
|
|
|
151
151
|
/**
|
|
152
152
|
* Safe number coercion for BigInt values.
|
|
153
153
|
*/
|
|
154
|
+
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
|
155
|
+
const MIN_SAFE = -MAX_SAFE;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Narrow a bigint to a number, refusing to do it lossily.
|
|
159
|
+
*
|
|
160
|
+
* Arrow int64/uint64 values arrive as bigint. Several bind-time argument paths
|
|
161
|
+
* want a plain number, and used to get one from a bare `Number(value)` — which
|
|
162
|
+
* silently rounds above 2^53, so an id or a nanosecond timestamp passed as a
|
|
163
|
+
* function argument came back subtly wrong with nothing logged anywhere.
|
|
164
|
+
*
|
|
165
|
+
* Throwing is the lesser evil: a caller that genuinely wants a lossy narrowing
|
|
166
|
+
* can still write `Number(v)` themselves, but nobody has to discover the loss
|
|
167
|
+
* from mismatched output weeks later.
|
|
168
|
+
*/
|
|
154
169
|
export function safeNumber(value: any): number {
|
|
155
170
|
if (typeof value === "bigint") {
|
|
171
|
+
if (value > MAX_SAFE || value < MIN_SAFE) {
|
|
172
|
+
throw new RangeError(
|
|
173
|
+
`${value} cannot be represented exactly as a JS number ` +
|
|
174
|
+
`(|value| > Number.MAX_SAFE_INTEGER). Keep it as a bigint.`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
156
177
|
return Number(value);
|
|
157
178
|
}
|
|
158
179
|
return value as number;
|
|
@@ -20,7 +20,9 @@ import {
|
|
|
20
20
|
Float32 as A_Float32,
|
|
21
21
|
Float64 as A_Float64,
|
|
22
22
|
Utf8 as A_Utf8,
|
|
23
|
+
LargeUtf8 as A_LargeUtf8,
|
|
23
24
|
Binary as A_Binary,
|
|
25
|
+
LargeBinary as A_LargeBinary,
|
|
24
26
|
Decimal as A_Decimal,
|
|
25
27
|
Date_ as A_Date,
|
|
26
28
|
Time as A_Time,
|
|
@@ -134,7 +136,9 @@ export const float32 = (): VgiDataType => new A_Float32() as unknown as VgiDataT
|
|
|
134
136
|
export const float64 = (): VgiDataType => new A_Float64() as unknown as VgiDataType;
|
|
135
137
|
|
|
136
138
|
export const utf8 = (): VgiDataType => new A_Utf8() as unknown as VgiDataType;
|
|
139
|
+
export const largeUtf8 = (): VgiDataType => new A_LargeUtf8() as unknown as VgiDataType;
|
|
137
140
|
export const binary = (): VgiDataType => new A_Binary() as unknown as VgiDataType;
|
|
141
|
+
export const largeBinary = (): VgiDataType => new A_LargeBinary() as unknown as VgiDataType;
|
|
138
142
|
export const fixedSizeBinary = (byteWidth: number): VgiDataType =>
|
|
139
143
|
new A_FixedSizeBinary(byteWidth) as unknown as VgiDataType;
|
|
140
144
|
|
|
@@ -22,7 +22,7 @@ export {
|
|
|
22
22
|
int8, int16, int32, int64,
|
|
23
23
|
uint8, uint16, uint32, uint64,
|
|
24
24
|
float16, float32, float64,
|
|
25
|
-
utf8, binary, fixedSizeBinary,
|
|
25
|
+
utf8, largeUtf8, binary, largeBinary, fixedSizeBinary,
|
|
26
26
|
decimal, decimal128, decimal256,
|
|
27
27
|
date, dateDay, dateMillisecond,
|
|
28
28
|
time, timeSecond, timeMillisecond, timeMicrosecond, timeNanosecond,
|
|
@@ -115,7 +115,30 @@ export function batchToSecretDict(
|
|
|
115
115
|
/**
|
|
116
116
|
* BigInt → number coercion (lossy for values past 2^53).
|
|
117
117
|
*/
|
|
118
|
+
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
|
119
|
+
const MIN_SAFE = -MAX_SAFE;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Narrow a bigint to a number, refusing to do it lossily.
|
|
123
|
+
*
|
|
124
|
+
* Arrow int64/uint64 values arrive as bigint. Several bind-time argument paths
|
|
125
|
+
* want a plain number, and used to get one from a bare `Number(value)` — which
|
|
126
|
+
* silently rounds above 2^53, so an id or a nanosecond timestamp passed as a
|
|
127
|
+
* function argument came back subtly wrong with nothing logged anywhere.
|
|
128
|
+
*
|
|
129
|
+
* Throwing is the lesser evil: a caller that genuinely wants a lossy narrowing
|
|
130
|
+
* can still write `Number(v)` themselves, but nobody has to discover the loss
|
|
131
|
+
* from mismatched output weeks later.
|
|
132
|
+
*/
|
|
118
133
|
export function safeNumber(value: any): number {
|
|
119
|
-
if (typeof value === "bigint")
|
|
134
|
+
if (typeof value === "bigint") {
|
|
135
|
+
if (value > MAX_SAFE || value < MIN_SAFE) {
|
|
136
|
+
throw new RangeError(
|
|
137
|
+
`${value} cannot be represented exactly as a JS number ` +
|
|
138
|
+
`(|value| > Number.MAX_SAFE_INTEGER). Keep it as a bigint.`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return Number(value);
|
|
142
|
+
}
|
|
120
143
|
return value as number;
|
|
121
144
|
}
|
|
@@ -23,7 +23,9 @@ import {
|
|
|
23
23
|
float32 as f_float32,
|
|
24
24
|
float64 as f_float64,
|
|
25
25
|
utf8 as f_utf8,
|
|
26
|
+
largeUtf8 as f_largeUtf8,
|
|
26
27
|
binary as f_binary,
|
|
28
|
+
largeBinary as f_largeBinary,
|
|
27
29
|
fixedSizeBinary as f_fixedSizeBinary,
|
|
28
30
|
decimal as f_decimal,
|
|
29
31
|
decimal128 as f_decimal128,
|
|
@@ -111,7 +113,9 @@ export const float32 = (): VgiDataType => f_float32() as unknown as VgiDataType;
|
|
|
111
113
|
export const float64 = (): VgiDataType => f_float64() as unknown as VgiDataType;
|
|
112
114
|
|
|
113
115
|
export const utf8 = (): VgiDataType => f_utf8() as unknown as VgiDataType;
|
|
116
|
+
export const largeUtf8 = (): VgiDataType => f_largeUtf8() as unknown as VgiDataType;
|
|
114
117
|
export const binary = (): VgiDataType => f_binary() as unknown as VgiDataType;
|
|
118
|
+
export const largeBinary = (): VgiDataType => f_largeBinary() as unknown as VgiDataType;
|
|
115
119
|
export const fixedSizeBinary = (stride: number): VgiDataType =>
|
|
116
120
|
f_fixedSizeBinary(stride) as unknown as VgiDataType;
|
|
117
121
|
|
package/src/arrow/index.ts
CHANGED
|
@@ -81,7 +81,7 @@ export {
|
|
|
81
81
|
int, int8, int16, int32, int64,
|
|
82
82
|
uint8, uint16, uint32, uint64,
|
|
83
83
|
float16, float32, float64,
|
|
84
|
-
utf8, binary, fixedSizeBinary,
|
|
84
|
+
utf8, largeUtf8, binary, largeBinary, fixedSizeBinary,
|
|
85
85
|
decimal, decimal128, decimal256,
|
|
86
86
|
date, dateDay, dateMillisecond,
|
|
87
87
|
time, timeSecond, timeMillisecond, timeMicrosecond, timeNanosecond,
|
|
@@ -25,7 +25,9 @@ import type {
|
|
|
25
25
|
IntType,
|
|
26
26
|
FloatType,
|
|
27
27
|
Utf8Type,
|
|
28
|
+
LargeUtf8Type,
|
|
28
29
|
BinaryType,
|
|
30
|
+
LargeBinaryType,
|
|
29
31
|
FixedSizeBinaryType,
|
|
30
32
|
DecimalType,
|
|
31
33
|
Date32Type, Date64Type,
|
|
@@ -83,7 +85,9 @@ export const float32 = (): FloatType => impl.float32() as unknown as FloatType;
|
|
|
83
85
|
export const float64 = (): FloatType => impl.float64() as unknown as FloatType;
|
|
84
86
|
|
|
85
87
|
export const utf8 = (): Utf8Type => impl.utf8() as unknown as Utf8Type;
|
|
88
|
+
export const largeUtf8 = (): LargeUtf8Type => impl.largeUtf8() as unknown as LargeUtf8Type;
|
|
86
89
|
export const binary = (): BinaryType => impl.binary() as unknown as BinaryType;
|
|
90
|
+
export const largeBinary = (): LargeBinaryType => impl.largeBinary() as unknown as LargeBinaryType;
|
|
87
91
|
export const fixedSizeBinary = (byteWidth: number): FixedSizeBinaryType =>
|
|
88
92
|
impl.fixedSizeBinary(byteWidth) as unknown as FixedSizeBinaryType;
|
|
89
93
|
|
package/src/arrow/types.ts
CHANGED
|
@@ -33,9 +33,43 @@ export interface VgiSchema {
|
|
|
33
33
|
readonly metadata: Map<string, string>;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
/**
|
|
37
|
+
* Column view over one column of a batch.
|
|
38
|
+
*
|
|
39
|
+
* The value type is erased by default: arrow-js parameterizes on DataType and
|
|
40
|
+
* flechette on value type, so the facade cannot name one without picking a
|
|
41
|
+
* backend. Supply `T` at the use site — the column's declared type makes it
|
|
42
|
+
* known there — and the cast disappears:
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* const ns = batch.getChildAt(0)! as Iterable<bigint | null>;
|
|
46
|
+
* for (const v of ns) { ... } // v: bigint | null
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* A type parameter here would read better, but it cannot be had cheaply: both
|
|
50
|
+
* backends' native batches are assigned to VgiBatch structurally, and a
|
|
51
|
+
* concrete `get(): unknown` does not satisfy a generic `get(): T`. Adding one
|
|
52
|
+
* would mean casting at ~25 internal sites to remove one cast in user code.
|
|
53
|
+
*
|
|
54
|
+
* **These are the backend's own values, not codec output.** `get()` and
|
|
55
|
+
* iteration return whatever the Arrow implementation stores, which for some
|
|
56
|
+
* types is not the value the SDK documents:
|
|
57
|
+
*
|
|
58
|
+
* | Arrow type | here | {@link iterRows} |
|
|
59
|
+
* | ---------------- | ----------------------------- | --------------------- |
|
|
60
|
+
* | `int64` | `bigint` | `bigint` (same) |
|
|
61
|
+
* | `utf8`, `float64`| `string`, `number` | same |
|
|
62
|
+
* | `decimal128` | backend limbs (`DecimalBigNum`)| `bigint`, unscaled |
|
|
63
|
+
* | `timestamp[us]` | millisecond `number` | microsecond `bigint` |
|
|
64
|
+
* | `date32` | millisecond `number` | `Date` |
|
|
65
|
+
*
|
|
66
|
+
* `repr: "raw"` on a function selects the *codec's* representation and has no
|
|
67
|
+
* effect on this path.
|
|
68
|
+
*
|
|
69
|
+
* So: reach for `getChildAt` on integer, float, boolean, string and binary
|
|
70
|
+
* columns, where it is the cheapest correct thing. For temporal, decimal and
|
|
71
|
+
* nested types go through {@link iterRows}, which runs the codec.
|
|
72
|
+
*/
|
|
39
73
|
export interface VgiColumn {
|
|
40
74
|
readonly type: VgiDataType;
|
|
41
75
|
readonly length: number;
|
|
@@ -46,7 +80,9 @@ export interface VgiColumn {
|
|
|
46
80
|
export interface VgiBatch {
|
|
47
81
|
readonly schema: VgiSchema;
|
|
48
82
|
readonly numRows: number;
|
|
83
|
+
/** See {@link VgiColumn} — this does NOT run the codec. */
|
|
49
84
|
getChild(name: string): VgiColumn | null;
|
|
85
|
+
/** See {@link VgiColumn} — this does NOT run the codec. */
|
|
50
86
|
getChildAt(index: number): VgiColumn | null;
|
|
51
87
|
}
|
|
52
88
|
|
package/src/catalog/interface.ts
CHANGED
|
@@ -164,7 +164,7 @@ export const encodeAttachCatalogInfo = (v: AttachCatalogInfo): Uint8Array =>
|
|
|
164
164
|
import { deserializeBatch, serializeBatch, batchFromColumns } from "../util/arrow/index.js";
|
|
165
165
|
import { schema as schema_, field as field_, type VgiDataType, type VgiField } from "../arrow/index.js";
|
|
166
166
|
import { toUint8Array } from "../util/bytes.js";
|
|
167
|
-
import { ScanBranchSchema } from "../generated/vgi-protocol-schemas.js";
|
|
167
|
+
import { ScanBranchSchema, ScanFunctionResultSchema } from "../generated/vgi-protocol-schemas.js";
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
170
|
* Result from `tableScanFunctionGet` — tells the VGI DuckDB extension which
|
|
@@ -181,6 +181,37 @@ export interface ScanFunctionResult {
|
|
|
181
181
|
requiredExtensions: string[];
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Encode a ScanFunctionResult wire record — the single-row batch shape
|
|
186
|
+
* `{ function_name, arguments, required_extensions }` that
|
|
187
|
+
* `catalog_table_scan_function_get` returns and that a function-backed table
|
|
188
|
+
* inlines into `TableInfo.scan_function`.
|
|
189
|
+
*
|
|
190
|
+
* It exists so there is exactly one place that names those columns. The
|
|
191
|
+
* inline-scan path used to restate the schema locally even though codegen
|
|
192
|
+
* emits it; a hand-written wire schema is how four SDKs ended up disagreeing
|
|
193
|
+
* about which columns were binary and which were large_binary.
|
|
194
|
+
*
|
|
195
|
+
* `argumentsBytes` is the already-serialized inner arguments batch (one column
|
|
196
|
+
* per argument: `arg_<index>` positional, bare name for named).
|
|
197
|
+
*/
|
|
198
|
+
export function encodeScanFunctionResult(
|
|
199
|
+
functionName: string,
|
|
200
|
+
argumentsBytes: Uint8Array,
|
|
201
|
+
requiredExtensions: string[] = [],
|
|
202
|
+
): Uint8Array {
|
|
203
|
+
return serializeBatch(
|
|
204
|
+
batchFromColumns(
|
|
205
|
+
{
|
|
206
|
+
function_name: [functionName],
|
|
207
|
+
arguments: [argumentsBytes],
|
|
208
|
+
required_extensions: [requiredExtensions],
|
|
209
|
+
},
|
|
210
|
+
ScanFunctionResultSchema as any,
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
184
215
|
/**
|
|
185
216
|
* Decode a ScanFunctionResult from a wire-shape inner result dict.
|
|
186
217
|
*
|
|
@@ -237,24 +268,59 @@ export function decodeScanFunctionResult(inner: Record<string, unknown>): ScanFu
|
|
|
237
268
|
export function singleBranchResult(
|
|
238
269
|
legacy: { function_name: string; arguments: unknown; required_extensions?: string[] },
|
|
239
270
|
): { branches: Uint8Array[]; required_extensions: string[] } {
|
|
240
|
-
const branchBatch = batchFromColumns(
|
|
241
|
-
{
|
|
242
|
-
function_name: [legacy.function_name],
|
|
243
|
-
arguments: [toUint8Array(legacy.arguments)],
|
|
244
|
-
branch_filter: [null],
|
|
245
|
-
writable: [false],
|
|
246
|
-
source_catalog: [null],
|
|
247
|
-
source_schema: [null],
|
|
248
|
-
source_table: [null],
|
|
249
|
-
},
|
|
250
|
-
ScanBranchSchema as any,
|
|
251
|
-
);
|
|
252
271
|
return {
|
|
253
|
-
branches: [
|
|
272
|
+
branches: [
|
|
273
|
+
encodeScanBranch({ function_name: legacy.function_name, arguments: toUint8Array(legacy.arguments) }),
|
|
274
|
+
],
|
|
254
275
|
required_extensions: legacy.required_extensions ?? [],
|
|
255
276
|
};
|
|
256
277
|
}
|
|
257
278
|
|
|
279
|
+
/**
|
|
280
|
+
* The ONE place `ScanBranch`'s columns are named.
|
|
281
|
+
*
|
|
282
|
+
* Both branch builders used to carry their own copy of this key list, and both
|
|
283
|
+
* copies were missing `format_name` / `format_locations` after those fields
|
|
284
|
+
* were added to the schema. `format_locations` is a LIST, and Arrow's assembler
|
|
285
|
+
* dereferences a list's `children[0]` while writing, so the omission did not
|
|
286
|
+
* degrade anything — it killed every scan through those builders with
|
|
287
|
+
* "Cannot read properties of undefined (reading 'slice')", including tables
|
|
288
|
+
* that predated format branches entirely. One key list is the fix; the
|
|
289
|
+
* completeness test in src/__tests__ pins it against ScanBranchSchema.
|
|
290
|
+
*
|
|
291
|
+
* Every column the schema declares must appear here, null included.
|
|
292
|
+
*/
|
|
293
|
+
function encodeScanBranch(cols: {
|
|
294
|
+
function_name: string;
|
|
295
|
+
arguments: Uint8Array;
|
|
296
|
+
branch_filter?: string | null;
|
|
297
|
+
writable?: boolean;
|
|
298
|
+
source_catalog?: string | null;
|
|
299
|
+
source_schema?: string | null;
|
|
300
|
+
source_table?: string | null;
|
|
301
|
+
format_name?: string | null;
|
|
302
|
+
format_locations?: string[] | null;
|
|
303
|
+
format_options?: Uint8Array | null;
|
|
304
|
+
}): Uint8Array {
|
|
305
|
+
return serializeBatch(
|
|
306
|
+
batchFromColumns(
|
|
307
|
+
{
|
|
308
|
+
function_name: [cols.function_name],
|
|
309
|
+
arguments: [cols.arguments],
|
|
310
|
+
branch_filter: [cols.branch_filter ?? null],
|
|
311
|
+
writable: [cols.writable ?? false],
|
|
312
|
+
source_catalog: [cols.source_catalog ?? null],
|
|
313
|
+
source_schema: [cols.source_schema ?? null],
|
|
314
|
+
source_table: [cols.source_table ?? null],
|
|
315
|
+
format_name: [cols.format_name ?? null],
|
|
316
|
+
format_locations: [cols.format_locations ?? null],
|
|
317
|
+
format_options: [cols.format_options ?? null],
|
|
318
|
+
},
|
|
319
|
+
ScanBranchSchema as any,
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
258
324
|
/**
|
|
259
325
|
* One physical source backing a multi-branch scan, in the shape the
|
|
260
326
|
* `buildScanBranchesResult` helper consumes. Mirrors vgi-python's
|
|
@@ -275,6 +341,16 @@ export interface ScanBranchInput {
|
|
|
275
341
|
branchFilter?: string | null;
|
|
276
342
|
/** Declares this branch as the INSERT target (at most one per table). */
|
|
277
343
|
writable?: boolean;
|
|
344
|
+
/** Custom file FORMAT this branch reads, for a format-backed branch. */
|
|
345
|
+
formatName?: string | null;
|
|
346
|
+
/** Locations the format branch reads. */
|
|
347
|
+
formatLocations?: string[] | null;
|
|
348
|
+
/** Reader options for a format branch, as a 1-row IPC batch whose COLUMN
|
|
349
|
+
* NAMES are the option names — the same shape `arguments` uses, because an
|
|
350
|
+
* option value may be any Arrow type. These BECOME the reader's named
|
|
351
|
+
* arguments, so without them a format branch cannot pass its reader a
|
|
352
|
+
* delimiter, a header flag, or anything else. */
|
|
353
|
+
formatOptions?: Uint8Array | null;
|
|
278
354
|
/**
|
|
279
355
|
* Catalog-table branch (lakehouse federation): leave `functionName` empty and
|
|
280
356
|
* set these to scan the base table
|
|
@@ -292,6 +368,26 @@ export interface ScanBranchInput {
|
|
|
292
368
|
* positional args, the bare name for named args. Mirrors Python's
|
|
293
369
|
* `ScanBranch.to_row_dict` argument handling.
|
|
294
370
|
*/
|
|
371
|
+
/**
|
|
372
|
+
* Encode a format branch's reader options.
|
|
373
|
+
*
|
|
374
|
+
* Same shape as a branch's `arguments` blob — a 1-row batch whose COLUMN NAMES
|
|
375
|
+
* are the option names — because an option value may be any Arrow type, so no
|
|
376
|
+
* static schema could describe the map. Exported because a worker building a
|
|
377
|
+
* format branch needs it and has no other way to produce those bytes.
|
|
378
|
+
*/
|
|
379
|
+
export function encodeFormatOptions(
|
|
380
|
+
options: Record<string, { value: unknown; type: VgiDataType }>,
|
|
381
|
+
): Uint8Array {
|
|
382
|
+
const fields: VgiField[] = [];
|
|
383
|
+
const values: Record<string, unknown[]> = {};
|
|
384
|
+
for (const [name, arg] of Object.entries(options)) {
|
|
385
|
+
fields.push(field_(name, arg.type, true));
|
|
386
|
+
values[name] = [arg.value];
|
|
387
|
+
}
|
|
388
|
+
return serializeBatch(batchFromColumns(values, schema_(fields)));
|
|
389
|
+
}
|
|
390
|
+
|
|
295
391
|
function serializeBranchArguments(branch: ScanBranchInput): Uint8Array {
|
|
296
392
|
const fields: VgiField[] = [];
|
|
297
393
|
const values: Record<string, unknown[]> = {};
|
|
@@ -328,21 +424,20 @@ export function buildScanBranchesResult(
|
|
|
328
424
|
branches: ScanBranchInput[],
|
|
329
425
|
requiredExtensions: string[] = [],
|
|
330
426
|
): { branches: Uint8Array[]; required_extensions: string[] } {
|
|
331
|
-
const serializedBranches = branches.map((branch) =>
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
});
|
|
427
|
+
const serializedBranches = branches.map((branch) =>
|
|
428
|
+
encodeScanBranch({
|
|
429
|
+
function_name: branch.functionName,
|
|
430
|
+
arguments: serializeBranchArguments(branch),
|
|
431
|
+
branch_filter: branch.branchFilter,
|
|
432
|
+
writable: branch.writable,
|
|
433
|
+
source_catalog: branch.sourceCatalog,
|
|
434
|
+
source_schema: branch.sourceSchema,
|
|
435
|
+
source_table: branch.sourceTable,
|
|
436
|
+
format_name: branch.formatName,
|
|
437
|
+
format_locations: branch.formatLocations,
|
|
438
|
+
format_options: branch.formatOptions,
|
|
439
|
+
}),
|
|
440
|
+
);
|
|
346
441
|
return {
|
|
347
442
|
branches: serializedBranches,
|
|
348
443
|
required_extensions: requiredExtensions,
|
package/src/catalog/read-only.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type IndexConstraintType,
|
|
18
18
|
type CopyFromFormatInfo,
|
|
19
19
|
encodeAttachCatalogInfo,
|
|
20
|
+
encodeScanFunctionResult,
|
|
20
21
|
encodeFunctionInfo,
|
|
21
22
|
} from "./interface.js";
|
|
22
23
|
import type { CatalogDescriptor, SchemaDescriptor, TableDescriptor, ViewDescriptor, MacroDescriptor, SettingDescriptor, SecretTypeDescriptor, ForeignKeyDef, DefaultValue } from "./descriptors.js";
|
|
@@ -473,6 +474,22 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
|
|
|
473
474
|
supports_batch_index:
|
|
474
475
|
((f.kind as string) === "table_buffering" && f.meta.requiresInputBatchIndex === true) ||
|
|
475
476
|
((f.kind as string) === "table" && f.meta.supportsBatchIndex === true),
|
|
477
|
+
// Split-based scan planning (protocol 1.4.0). A function opts in by
|
|
478
|
+
// declaring `supportsSplits` AND implementing plan()/onSplit(): the
|
|
479
|
+
// declaration is what a distributed engine reads to decide it can
|
|
480
|
+
// retry a task, because a split NAMES its work and re-running one
|
|
481
|
+
// reads exactly the same rows.
|
|
482
|
+
supports_splits: (f.kind as string) === "table" && f.meta.supportsSplits === true,
|
|
483
|
+
// The worker applies pushed filters EXACTLY, so the engine may drop
|
|
484
|
+
// its own copy. Wrong answers if declared falsely.
|
|
485
|
+
filters_exactly_applied:
|
|
486
|
+
(f.kind as string) === "table" && f.meta.filtersExactlyApplied === true,
|
|
487
|
+
// Addressable positions in the data, for incremental/streaming reads.
|
|
488
|
+
supports_positions: (f.kind as string) === "table" && f.meta.supportsPositions === true,
|
|
489
|
+
// null means UNBOUNDED, not "expires immediately" — a client must not
|
|
490
|
+
// assume a TTL exists, or long-running streams are foreclosed.
|
|
491
|
+
split_token_ttl_seconds:
|
|
492
|
+
(f.kind as string) === "table" ? (f.meta.splitTokenTtlSeconds ?? null) : null,
|
|
476
493
|
partition_kind:
|
|
477
494
|
(f.kind as string) === "table" && f.meta.partitionKind
|
|
478
495
|
? f.meta.partitionKind
|
|
@@ -993,22 +1010,5 @@ function inlineScanFunction(
|
|
|
993
1010
|
): Uint8Array {
|
|
994
1011
|
const argSchema = argumentSpecsToSchema(func.argumentSpecs);
|
|
995
1012
|
const argBytes = serializeArgsBatch(args ?? new Arguments(), argSchema);
|
|
996
|
-
|
|
997
|
-
field("function_name", utf8(), false),
|
|
998
|
-
field("arguments", binary(), false),
|
|
999
|
-
field(
|
|
1000
|
-
"required_extensions",
|
|
1001
|
-
list(field("item", utf8(), true)),
|
|
1002
|
-
false,
|
|
1003
|
-
),
|
|
1004
|
-
]);
|
|
1005
|
-
const batch = batchFromColumns(
|
|
1006
|
-
{
|
|
1007
|
-
function_name: [func.meta.name],
|
|
1008
|
-
arguments: [argBytes],
|
|
1009
|
-
required_extensions: [[] as string[]],
|
|
1010
|
-
},
|
|
1011
|
-
scanSchema,
|
|
1012
|
-
);
|
|
1013
|
-
return serializeBatch(batch);
|
|
1013
|
+
return encodeScanFunctionResult(func.meta.name, argBytes);
|
|
1014
1014
|
}
|
package/src/client/client.ts
CHANGED
|
@@ -70,7 +70,7 @@ import type {
|
|
|
70
70
|
* Works with any RpcClient transport (subprocess or HTTP):
|
|
71
71
|
* ```ts
|
|
72
72
|
* import { subprocessConnect } from "@query-farm/vgi-rpc";
|
|
73
|
-
* import { VgiClient, Arguments } from "vgi";
|
|
73
|
+
* import { VgiClient, Arguments } from "@query-farm/vgi";
|
|
74
74
|
*
|
|
75
75
|
* const rpc = subprocessConnect(["./my-worker"]);
|
|
76
76
|
* const client = new VgiClient(rpc);
|
|
@@ -157,6 +157,10 @@ export class VgiClient {
|
|
|
157
157
|
bind_call: bindRequest,
|
|
158
158
|
output_schema: bindResponse.output_schema,
|
|
159
159
|
bind_opaque_data: bindResponse.opaque_data,
|
|
160
|
+
// This client does not plan scans, so it never redeems a split, and it
|
|
161
|
+
// pushes no limit down.
|
|
162
|
+
split_tokens: null,
|
|
163
|
+
row_limit: null,
|
|
160
164
|
projection_ids: opts?.projectionIds ?? null,
|
|
161
165
|
pushdown_filters: opts?.pushdownFilters ?? null,
|
|
162
166
|
join_keys: opts?.joinKeys ?? [],
|
|
@@ -1302,7 +1306,7 @@ export class VgiClient {
|
|
|
1302
1306
|
transactionOpaqueData?: TransactionOpaqueData,
|
|
1303
1307
|
): Promise<void> {
|
|
1304
1308
|
const schema = schema_([
|
|
1305
|
-
field("attach_opaque_data", binary(),
|
|
1309
|
+
field("attach_opaque_data", binary(), false),
|
|
1306
1310
|
field("schema_name", utf8(), false),
|
|
1307
1311
|
field("name", utf8(), false),
|
|
1308
1312
|
field("macro_type", utf8(), false),
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type ArgumentSpec,
|
|
21
21
|
type ArgumentConstraints,
|
|
22
22
|
} from "../arguments/argument-spec.js";
|
|
23
|
+
import { assertArrowType, assertArrowTypes } from "../arguments/argument-spec.js";
|
|
23
24
|
import type {
|
|
24
25
|
FunctionMeta,
|
|
25
26
|
VgiFunction,
|
|
@@ -333,6 +334,8 @@ export function deleteExecutionState(executionId: Uint8Array): void {
|
|
|
333
334
|
export function defineAggregate<TArgs = Record<string, any>, TState = any>(
|
|
334
335
|
config: AggregateFunctionConfig<TArgs, TState>,
|
|
335
336
|
): VgiFunction & { aggregateConfig: AggregateFunctionConfig<TArgs, TState> } {
|
|
337
|
+
assertArrowTypes((config as any).args, `defineAggregate("${config.name}"): args`);
|
|
338
|
+
|
|
336
339
|
const specs: ArgumentSpec[] = [];
|
|
337
340
|
let posIdx = 0;
|
|
338
341
|
const varargsSet = new Set(config.varargs ?? []);
|
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
import type { VgiFunction, FunctionMeta, StreamHandlers, FunctionExample } from "./types.js";
|
|
32
32
|
import type { CopySecretLookup } from "./copy-to.js";
|
|
33
33
|
import type { ArgumentSpec } from "../arguments/argument-spec.js";
|
|
34
|
+
import { narrowArgValue } from "../arguments/argument-spec.js";
|
|
34
35
|
import { batchToScalarDict, batchToSecretDict, safeNumber } from "../util/arrow/index.js";
|
|
35
36
|
import { BoundStorage, storage as globalStorage } from "./storage.js";
|
|
36
37
|
import type { TableProcessParams } from "./table.js";
|
|
@@ -165,7 +166,7 @@ export function defineCopyFromFunction<TArgs = Record<string, unknown>>(
|
|
|
165
166
|
}
|
|
166
167
|
val = opt.default;
|
|
167
168
|
}
|
|
168
|
-
|
|
169
|
+
val = narrowArgValue(val);
|
|
169
170
|
if (opt.choices && !opt.choices.includes(val)) {
|
|
170
171
|
throw new Error(
|
|
171
172
|
`COPY (FORMAT ${config.format}): option '${name}' value ${JSON.stringify(val)} ` +
|
package/src/functions/copy-to.ts
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import { type VgiSchema, schema, type VgiBatch, type VgiDataType } from "../arrow/index.js";
|
|
39
39
|
import type { BindRequest } from "../protocol/types.js";
|
|
40
40
|
import type { FunctionMeta, FunctionExample } from "./types.js";
|
|
41
|
+
import { narrowArgValue } from "../arguments/argument-spec.js";
|
|
41
42
|
import { safeNumber } from "../util/arrow/index.js";
|
|
42
43
|
import {
|
|
43
44
|
defineTableBufferingFunction,
|
|
@@ -200,7 +201,7 @@ export function defineCopyToFunction<TArgs = Record<string, unknown>>(
|
|
|
200
201
|
}
|
|
201
202
|
val = opt.default;
|
|
202
203
|
}
|
|
203
|
-
|
|
204
|
+
val = narrowArgValue(val);
|
|
204
205
|
if (opt.choices && !opt.choices.includes(val)) {
|
|
205
206
|
throw new Error(
|
|
206
207
|
`COPY (FORMAT ${config.format}): option '${name}' value ${JSON.stringify(val)} ` +
|