@query-farm/vgi 0.29.0 → 0.29.1
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 -1
- package/dist/arguments/argument-spec.d.ts +4 -2
- package/dist/catalog/composite.d.ts +13 -13
- package/dist/catalog/descriptors.d.ts +7 -3
- package/dist/catalog/interface.d.ts +67 -40
- package/dist/catalog/read-only.d.ts +32 -15
- package/dist/client/client.d.ts +36 -29
- package/dist/client/types.d.ts +2 -0
- package/dist/client-entry.d.ts +1 -1
- package/dist/client-entry.js +441 -279
- package/dist/client-entry.js.map +12 -11
- package/dist/filter-pushdown/capabilities.d.ts +3 -0
- package/dist/filter-pushdown/deserialize.d.ts +14 -15
- package/dist/filter-pushdown/evaluate.d.ts +14 -44
- package/dist/filter-pushdown/index.d.ts +2 -2
- package/dist/filter-pushdown/types.d.ts +120 -68
- package/dist/functions/aggregate.d.ts +4 -0
- package/dist/functions/registry.d.ts +5 -5
- package/dist/functions/scalar.d.ts +6 -1
- package/dist/functions/table-buffering.d.ts +9 -0
- package/dist/functions/table-in-out.d.ts +13 -0
- package/dist/functions/table.d.ts +9 -2
- package/dist/functions/types.d.ts +22 -2
- package/dist/generated/vgi-client.d.ts +56 -27
- package/dist/generated/vgi-protocol-schemas.d.ts +15 -0
- package/dist/http/fetch.d.ts +7 -1
- package/dist/index.core.d.ts +2 -1
- package/dist/index.js +2317 -1354
- package/dist/index.js.map +43 -40
- package/dist/metadata/types.d.ts +15 -2
- package/dist/protocol/handlers/shared.d.ts +3 -1
- package/dist/protocol/serializers/splits.d.ts +1 -1
- package/dist/protocol/types.d.ts +9 -3
- package/dist/schema-path.d.ts +7 -0
- package/dist/serve-entry.d.ts +22 -1
- package/dist/serve-entry.js +412 -238
- package/dist/serve-entry.js.map +27 -26
- package/dist/split-token.d.ts +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/worker-cf-entry.js +2030 -1201
- package/dist/worker-cf-entry.js.map +40 -36
- package/dist/worker.d.ts +1 -1
- package/package.json +3 -3
- package/src/arguments/argument-spec.ts +12 -4
- package/src/catalog/composite.ts +26 -26
- package/src/catalog/descriptors.ts +12 -3
- package/src/catalog/interface.ts +98 -48
- package/src/catalog/read-only.ts +124 -67
- package/src/client/client.ts +92 -72
- package/src/client/types.ts +2 -0
- package/src/client-entry.ts +2 -1
- package/src/filter-pushdown/capabilities.ts +13 -0
- package/src/filter-pushdown/collector.ts +22 -35
- package/src/filter-pushdown/deserialize.ts +657 -221
- package/src/filter-pushdown/evaluate.ts +410 -499
- package/src/filter-pushdown/index.ts +16 -2
- package/src/filter-pushdown/types.ts +88 -71
- package/src/functions/aggregate.ts +9 -1
- package/src/functions/registry.ts +24 -22
- package/src/functions/scalar.ts +26 -0
- package/src/functions/table-buffering.ts +46 -7
- package/src/functions/table-in-out.ts +187 -30
- package/src/functions/table.ts +58 -20
- package/src/functions/types.ts +24 -1
- package/src/generated/vgi-client.ts +61 -29
- package/src/generated/vgi-protocol-schemas.ts +225 -54
- package/src/http/fetch.ts +12 -0
- package/src/index.core.ts +2 -0
- package/src/metadata/resolve.ts +43 -1
- package/src/metadata/serialize.ts +43 -5
- package/src/metadata/types.ts +17 -1
- package/src/protocol/dispatch.ts +1 -1
- package/src/protocol/handlers/aggregate.ts +13 -7
- package/src/protocol/handlers/catalog/admin.ts +6 -6
- package/src/protocol/handlers/catalog/index_methods.ts +2 -2
- package/src/protocol/handlers/catalog/macro.ts +4 -4
- package/src/protocol/handlers/catalog/shared.ts +2 -0
- package/src/protocol/handlers/catalog/table.ts +16 -16
- package/src/protocol/handlers/catalog/view.ts +5 -5
- package/src/protocol/handlers/function.ts +2 -2
- package/src/protocol/handlers/shared.ts +12 -3
- package/src/protocol/handlers/table-buffering.ts +4 -4
- package/src/protocol/serializers/bind.ts +15 -6
- package/src/protocol/serializers/splits.ts +4 -2
- package/src/protocol/types.ts +9 -3
- package/src/schema-path.ts +25 -0
- package/src/serve-entry.ts +65 -3
- package/src/split-token.ts +2 -2
- package/src/types.ts +9 -0
- package/src/worker.ts +49 -4
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Two-phase: INPUT phase receives and transforms batches,
|
|
4
4
|
// FINALIZE phase emits final results.
|
|
5
5
|
|
|
6
|
-
import { type VgiSchema, schema, type VgiField, type VgiDataType, type VgiBatch, nullType, withBatchMetadata } from "../arrow/index.js";
|
|
6
|
+
import { type VgiSchema, schema, type VgiField, type VgiDataType, type VgiBatch, nullType, withBatchMetadata, serializeBatch, deserializeBatch } from "../arrow/index.js";
|
|
7
7
|
import type { OutputCollector } from "@query-farm/vgi-rpc";
|
|
8
8
|
import { DEFAULT_MAX_WORKERS, TableInOutPhase } from "../types.js";
|
|
9
9
|
import type {
|
|
@@ -18,20 +18,33 @@ import type {
|
|
|
18
18
|
StreamHandlers,
|
|
19
19
|
FunctionExample,
|
|
20
20
|
} from "./types.js";
|
|
21
|
-
import type { ArgumentSpec } from "../arguments/argument-spec.js";
|
|
22
|
-
import {
|
|
21
|
+
import type { ArgumentConstraints, ArgumentSpec } from "../arguments/argument-spec.js";
|
|
22
|
+
import {
|
|
23
|
+
assertArrowType,
|
|
24
|
+
assertArrowTypes,
|
|
25
|
+
constraintSpecFields,
|
|
26
|
+
narrowArgValue,
|
|
27
|
+
validateConstConstraints,
|
|
28
|
+
} from "../arguments/argument-spec.js";
|
|
23
29
|
import { batchToScalarDict, batchToSecretDict, projectSchema, projectBatch, emptyBatch, safeNumber } from "../util/arrow/index.js";
|
|
24
30
|
import { CACHE_IF_MODIFIED_SINCE_KEY, CACHE_IF_NONE_MATCH_KEY } from "../cache-control.js";
|
|
25
31
|
import {
|
|
26
|
-
buildJoinKeysLookup,
|
|
27
32
|
deserializeFilters,
|
|
28
33
|
FilteringOutputCollector,
|
|
29
34
|
type PushdownFilters,
|
|
30
35
|
} from "../filter-pushdown/index.js";
|
|
31
36
|
import { FunctionStability } from "../types.js";
|
|
32
|
-
import { BoundStorage, storage as defaultStorage } from "./storage.js";
|
|
37
|
+
import { BoundStorage, storage as defaultStorage, FrameworkNS } from "./storage.js";
|
|
33
38
|
import { serializeUserState, deserializeUserState } from "../protocol/state-serializer.js";
|
|
34
39
|
|
|
40
|
+
/**
|
|
41
|
+
* State-log key the streaming FINALIZE drain uses within
|
|
42
|
+
* {@link FrameworkNS.STREAMING_FINALIZE}. There is exactly one flush per
|
|
43
|
+
* execution, so a single empty key is enough — matches vgi-python's
|
|
44
|
+
* `_STREAMING_FINALIZE_KEY = b""`.
|
|
45
|
+
*/
|
|
46
|
+
const STREAMING_FINALIZE_KEY = new Uint8Array(0);
|
|
47
|
+
|
|
35
48
|
// ============================================================================
|
|
36
49
|
// Table In-Out parameter bundles (reuse table function's)
|
|
37
50
|
// ============================================================================
|
|
@@ -41,6 +54,8 @@ export interface TableInOutBindParams<TArgs = Record<string, any>> {
|
|
|
41
54
|
bindCall: BindRequest;
|
|
42
55
|
settings: Record<string, any>;
|
|
43
56
|
secrets: Record<string, Record<string, any>>;
|
|
57
|
+
/** Resolved names aligned with the complete logical argument order. */
|
|
58
|
+
argumentNames?: (string | null)[] | null;
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
export interface TableInOutProcessParams<TArgs = Record<string, any>> {
|
|
@@ -92,6 +107,12 @@ export interface TableInOutConfig<
|
|
|
92
107
|
namedArgs?: Record<string, VgiDataType>;
|
|
93
108
|
/** Argument defaults */
|
|
94
109
|
argDefaults?: Record<string, any>;
|
|
110
|
+
/** Authoritative typed defaults: one row, defaulted parameters only. */
|
|
111
|
+
parameterDefaultValues?: VgiBatch | null;
|
|
112
|
+
/** Argument descriptions keyed by argument name. */
|
|
113
|
+
argDocs?: Record<string, string>;
|
|
114
|
+
/** Discovery constraints, also enforced for scalar arguments at bind. */
|
|
115
|
+
argConstraints?: Record<string, ArgumentConstraints>;
|
|
95
116
|
/** Bind: default passes through input schema. May be async. */
|
|
96
117
|
onBind?: (params: TableInOutBindParams<TArgs>) =>
|
|
97
118
|
| { outputSchema: VgiSchema; opaqueData?: Uint8Array }
|
|
@@ -147,6 +168,11 @@ export function defineTableInOutFunction<
|
|
|
147
168
|
name,
|
|
148
169
|
position: posIdx++,
|
|
149
170
|
arrowType: type,
|
|
171
|
+
doc: config.argDocs?.[name],
|
|
172
|
+
...constraintSpecFields(
|
|
173
|
+
config.argConstraints?.[name],
|
|
174
|
+
config.argDefaults?.[name],
|
|
175
|
+
),
|
|
150
176
|
});
|
|
151
177
|
}
|
|
152
178
|
}
|
|
@@ -165,6 +191,11 @@ export function defineTableInOutFunction<
|
|
|
165
191
|
name,
|
|
166
192
|
position: name, // string position = named arg
|
|
167
193
|
arrowType: type,
|
|
194
|
+
doc: config.argDocs?.[name],
|
|
195
|
+
...constraintSpecFields(
|
|
196
|
+
config.argConstraints?.[name],
|
|
197
|
+
config.argDefaults?.[name],
|
|
198
|
+
),
|
|
168
199
|
});
|
|
169
200
|
}
|
|
170
201
|
}
|
|
@@ -179,6 +210,7 @@ export function defineTableInOutFunction<
|
|
|
179
210
|
examples: config.examples,
|
|
180
211
|
categories: config.categories,
|
|
181
212
|
tags: config.tags,
|
|
213
|
+
parameterDefaultValues: config.parameterDefaultValues,
|
|
182
214
|
maxWorkers: config.maxWorkers,
|
|
183
215
|
requiredSettings: config.requiredSettings,
|
|
184
216
|
requiredSecrets: config.requiredSecrets,
|
|
@@ -198,6 +230,8 @@ export function defineTableInOutFunction<
|
|
|
198
230
|
// Arrow Int64 arrives as bigint. Narrow when lossless, keep the
|
|
199
231
|
// bigint when not — see narrowArgValue.
|
|
200
232
|
val = narrowArgValue(val);
|
|
233
|
+
const constraints = config.argConstraints?.[spec.name];
|
|
234
|
+
if (constraints) validateConstConstraints(spec.name, constraints, val);
|
|
201
235
|
args[spec.name] = val;
|
|
202
236
|
}
|
|
203
237
|
return args as TArgs;
|
|
@@ -218,6 +252,7 @@ export function defineTableInOutFunction<
|
|
|
218
252
|
bindCall: request,
|
|
219
253
|
settings,
|
|
220
254
|
secrets,
|
|
255
|
+
argumentNames: request.argument_names ?? null,
|
|
221
256
|
});
|
|
222
257
|
return {
|
|
223
258
|
output_schema: result.outputSchema,
|
|
@@ -280,9 +315,14 @@ export function defineTableInOutFunction<
|
|
|
280
315
|
// Deserialize pushdown filters. Pass a join-keys column lookup so that
|
|
281
316
|
// filters DuckDB promoted to join_keys (IN/OR lists, etc.) are
|
|
282
317
|
// materialized as InFilters rather than silently dropped.
|
|
283
|
-
const joinKeysLookup = buildJoinKeysLookup(request.join_keys);
|
|
284
318
|
const pushdownFilters = request.pushdown_filters
|
|
285
|
-
? deserializeFilters(request.pushdown_filters,
|
|
319
|
+
? deserializeFilters(request.pushdown_filters, {
|
|
320
|
+
outputSchema: request.output_schema,
|
|
321
|
+
joinKeyBatches: request.join_keys,
|
|
322
|
+
extensionFunctions: meta.additionalFilterFunctions,
|
|
323
|
+
runtimeAlgorithms: meta.runtimeFilterAlgorithms,
|
|
324
|
+
evaluationContexts: meta.filterEvaluationContexts,
|
|
325
|
+
})
|
|
286
326
|
: undefined;
|
|
287
327
|
|
|
288
328
|
// Create BoundStorage for cross-phase/cross-worker data sharing
|
|
@@ -306,18 +346,41 @@ export function defineTableInOutFunction<
|
|
|
306
346
|
const phase = request.phase;
|
|
307
347
|
|
|
308
348
|
if (phase === TableInOutPhase.FINALIZE) {
|
|
309
|
-
// FINALIZE phase: producer mode
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
349
|
+
// FINALIZE phase: producer mode, drained by cursor out of the
|
|
350
|
+
// execution-scoped state log.
|
|
351
|
+
//
|
|
352
|
+
// Over HTTP a producer stream is strictly LOCK-STEP: at most one data
|
|
353
|
+
// batch per response, after which the worker hands back a continuation
|
|
354
|
+
// token and the client comes back for the next batch. A finalize()
|
|
355
|
+
// returning N batches therefore has to survive N-1 continuations — and
|
|
356
|
+
// a continuation rebuilds these handlers from scratch, with nothing of
|
|
357
|
+
// the previous turn left in memory.
|
|
358
|
+
//
|
|
359
|
+
// The prior shape kept the materialized batches on the handler object
|
|
360
|
+
// (a `batches` field beside `state`), and only `state` rides the state
|
|
361
|
+
// token. So the continuation found `batches == null` and re-ran the
|
|
362
|
+
// user's finalize() — whose inputs `boundStorage.collect()` had already
|
|
363
|
+
// DESTRUCTIVELY drained (DELETE ... RETURNING). Second call: no states,
|
|
364
|
+
// zero batches, immediate finish. The stream truncated after its first
|
|
365
|
+
// batch, silently, and only over HTTP (a byte-stream transport runs
|
|
366
|
+
// every tick inside one process, so the field survived).
|
|
367
|
+
//
|
|
368
|
+
// Same fix as vgi-python's BufferedFinalizeState: call the user's
|
|
369
|
+
// finalize() exactly ONCE, append each returned batch's IPC bytes to
|
|
370
|
+
// the state log under FrameworkNS.STREAMING_FINALIZE, and have every
|
|
371
|
+
// tick drain exactly ONE row past a cursor. Only the POSITION rides the
|
|
372
|
+
// state token, never the rows. finalize() is never re-run: its contract
|
|
373
|
+
// is to DRAIN accumulated state, so a second call is not obliged to
|
|
374
|
+
// return the same rows.
|
|
375
|
+
const finalizeLogKey = STREAMING_FINALIZE_KEY;
|
|
313
376
|
return {
|
|
314
377
|
outputSchema,
|
|
315
|
-
producerInit: () => ({ state: {
|
|
378
|
+
producerInit: () => ({ state: { cursor: -1, materialized: false } }),
|
|
316
379
|
producerFn: async (
|
|
317
|
-
pState: { state: {
|
|
380
|
+
pState: { state: { cursor: number; materialized: boolean } },
|
|
318
381
|
out: OutputCollector
|
|
319
382
|
) => {
|
|
320
|
-
if (pState.
|
|
383
|
+
if (!pState.state.materialized) {
|
|
321
384
|
const finalizeStates: TState[] = [];
|
|
322
385
|
if (accumulatedState != null) {
|
|
323
386
|
finalizeStates.push(accumulatedState as TState);
|
|
@@ -336,24 +399,55 @@ export function defineTableInOutFunction<
|
|
|
336
399
|
: (null as TState),
|
|
337
400
|
);
|
|
338
401
|
}
|
|
339
|
-
|
|
402
|
+
const batches: VgiBatch[] = config.finalize
|
|
340
403
|
? await config.finalize(processParams, finalizeStates)
|
|
341
404
|
: [];
|
|
405
|
+
for (const batch of batches) {
|
|
406
|
+
await boundStorage.stateAppend(
|
|
407
|
+
FrameworkNS.STREAMING_FINALIZE,
|
|
408
|
+
finalizeLogKey,
|
|
409
|
+
serializeBatch(batch),
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
// Set before the first emit so the flag is part of the userState
|
|
413
|
+
// this turn serializes — a continuation must never re-run
|
|
414
|
+
// finalize(), even when the log came back empty.
|
|
415
|
+
pState.state.materialized = true;
|
|
342
416
|
}
|
|
343
|
-
|
|
417
|
+
const rows = await boundStorage.stateLogScan(
|
|
418
|
+
FrameworkNS.STREAMING_FINALIZE,
|
|
419
|
+
finalizeLogKey,
|
|
420
|
+
pState.state.cursor,
|
|
421
|
+
1,
|
|
422
|
+
);
|
|
423
|
+
if (rows.length === 0) {
|
|
344
424
|
out.finish();
|
|
345
425
|
return;
|
|
346
426
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
out.finish();
|
|
351
|
-
}
|
|
427
|
+
const [logId, bytes] = rows[0];
|
|
428
|
+
out.emit(deserializeBatch(bytes) as unknown as VgiBatch);
|
|
429
|
+
pState.state.cursor = logId;
|
|
352
430
|
},
|
|
353
431
|
};
|
|
354
432
|
}
|
|
355
433
|
|
|
356
|
-
// INPUT phase: exchange mode
|
|
434
|
+
// INPUT phase: exchange mode. Any phase other than FINALIZE falls
|
|
435
|
+
// through to here, including a missing/undefined phase -- i.e. a
|
|
436
|
+
// caller that drove this function via table_function() instead of
|
|
437
|
+
// table_in_out_function(input=...). Reject that immediately rather
|
|
438
|
+
// than silently degrading to an empty inputSchema below, which
|
|
439
|
+
// disables the transport layer's own schema-conformance check and
|
|
440
|
+
// leaves both sides waiting on each other forever (confirmed live: a
|
|
441
|
+
// non-terminating exchange/continuation loop against a real deployed
|
|
442
|
+
// worker, not an error).
|
|
443
|
+
if (request.bind_call.input_schema == null) {
|
|
444
|
+
throw new Error(
|
|
445
|
+
`${config.name}: is a table-in-out function (it requires an input row ` +
|
|
446
|
+
`stream) but no input schema was supplied -- call it via ` +
|
|
447
|
+
`table_in_out_function(input=...), not table_function().`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
357
451
|
const state = config.initialState
|
|
358
452
|
? config.initialState(processParams)
|
|
359
453
|
: (null as TState);
|
|
@@ -372,7 +466,7 @@ export function defineTableInOutFunction<
|
|
|
372
466
|
|
|
373
467
|
return {
|
|
374
468
|
outputSchema,
|
|
375
|
-
inputSchema: request.bind_call.input_schema
|
|
469
|
+
inputSchema: request.bind_call.input_schema,
|
|
376
470
|
exchangeInit: () => ({ state, processParams }),
|
|
377
471
|
exchangeFn: async (
|
|
378
472
|
eState: {
|
|
@@ -386,6 +480,7 @@ export function defineTableInOutFunction<
|
|
|
386
480
|
// custom metadata (attached by the C++ WriteInputBatch), so process()
|
|
387
481
|
// can answer a 0-row `notModified` batch instead of recomputing.
|
|
388
482
|
applyRevalidationValidators(eState.processParams, input);
|
|
483
|
+
applyDynamicFilterUpdate(eState.processParams, input);
|
|
389
484
|
// Bake emit metadata onto the batch (0-row HTTP survival), then
|
|
390
485
|
// reconcile emitted batches to the (possibly projected) output schema
|
|
391
486
|
// by name — a process() may emit its full declared schema and let the
|
|
@@ -394,8 +489,9 @@ export function defineTableInOutFunction<
|
|
|
394
489
|
if (projIds) {
|
|
395
490
|
wrappedOut = makeSchemaReconcilingCollector(wrappedOut, outputSchema);
|
|
396
491
|
}
|
|
397
|
-
|
|
398
|
-
|
|
492
|
+
const currentFilters = eState.processParams.pushdownFilters;
|
|
493
|
+
if (config.autoApplyFilters && currentFilters) {
|
|
494
|
+
wrappedOut = new FilteringOutputCollector(wrappedOut, currentFilters) as unknown as OutputCollector;
|
|
399
495
|
}
|
|
400
496
|
await processFn(eState.processParams, eState.state, input, wrappedOut);
|
|
401
497
|
|
|
@@ -433,6 +529,29 @@ function applyRevalidationValidators(
|
|
|
433
529
|
params.ifModifiedSince = md?.get(CACHE_IF_MODIFIED_SINCE_KEY);
|
|
434
530
|
}
|
|
435
531
|
|
|
532
|
+
function applyDynamicFilterUpdate(
|
|
533
|
+
params: { pushdownFilters?: PushdownFilters },
|
|
534
|
+
input: VgiBatch,
|
|
535
|
+
): void {
|
|
536
|
+
const encoded = ((input as any)?.metadata as Map<string, string> | undefined)?.get("vgi_pushdown_filters");
|
|
537
|
+
if (!encoded) return;
|
|
538
|
+
if (!params.pushdownFilters) throw new Error("dynamic filter delta received without an initial snapshot");
|
|
539
|
+
if (new TextEncoder().encode(encoded).byteLength > (24 << 20)) {
|
|
540
|
+
throw new Error("base64 dynamic filter metadata exceeds the encoded-size limit");
|
|
541
|
+
}
|
|
542
|
+
if (encoded.length % 4 !== 0 ||
|
|
543
|
+
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded)) {
|
|
544
|
+
throw new Error("dynamic filter metadata is not valid base64");
|
|
545
|
+
}
|
|
546
|
+
const binary = (globalThis as any).atob
|
|
547
|
+
? (globalThis as any).atob(encoded)
|
|
548
|
+
: Buffer.from(encoded, "base64").toString("binary");
|
|
549
|
+
const bytes = Uint8Array.from(binary, (character: string) => character.charCodeAt(0));
|
|
550
|
+
if (bytes.byteLength > (17 << 20)) throw new Error("dynamic filter IPC payload exceeds the encoded-size limit");
|
|
551
|
+
const current = params.pushdownFilters;
|
|
552
|
+
params.pushdownFilters = current.applyDelta(deserializeBatch(bytes));
|
|
553
|
+
}
|
|
554
|
+
|
|
436
555
|
/**
|
|
437
556
|
* Wrap an OutputCollector so a pre-built batch emitted WITH metadata carries
|
|
438
557
|
* that metadata on the batch object itself (merged over any metadata the
|
|
@@ -619,8 +738,12 @@ export interface RowTransformConfig<TArgs = Record<string, any>> {
|
|
|
619
738
|
/** Named (string-position) args stay bind-time scalars on `params.args`. */
|
|
620
739
|
namedArgs?: Record<string, VgiDataType>;
|
|
621
740
|
argDefaults?: Record<string, any>;
|
|
741
|
+
/** Authoritative typed defaults: one row, defaulted parameters only. */
|
|
742
|
+
parameterDefaultValues?: VgiBatch | null;
|
|
622
743
|
/** Per-argument descriptions keyed by arg name (surfaced as `vgi_doc`). */
|
|
623
744
|
argDocs?: Record<string, string>;
|
|
745
|
+
/** Discovery constraints, also enforced for named scalar arguments at bind. */
|
|
746
|
+
argConstraints?: Record<string, ArgumentConstraints>;
|
|
624
747
|
/** Bind: return the output schema. The input schema (the declared per-row
|
|
625
748
|
* columns, typed by the C++ bind) is on `params.bindCall.input_schema`. */
|
|
626
749
|
onBind: (params: TableInOutBindParams<TArgs>) =>
|
|
@@ -687,6 +810,7 @@ export function defineRowTransformFunction<
|
|
|
687
810
|
position: posIdx++,
|
|
688
811
|
arrowType: type,
|
|
689
812
|
doc: config.argDocs?.[name],
|
|
813
|
+
...constraintSpecFields(config.argConstraints?.[name]),
|
|
690
814
|
});
|
|
691
815
|
}
|
|
692
816
|
}
|
|
@@ -697,6 +821,7 @@ export function defineRowTransformFunction<
|
|
|
697
821
|
arrowType: config.varargs.type,
|
|
698
822
|
isVarargs: true,
|
|
699
823
|
doc: config.varargs.doc ?? config.argDocs?.[config.varargs.name],
|
|
824
|
+
...constraintSpecFields(config.argConstraints?.[config.varargs.name]),
|
|
700
825
|
});
|
|
701
826
|
}
|
|
702
827
|
if (config.namedArgs) {
|
|
@@ -706,6 +831,10 @@ export function defineRowTransformFunction<
|
|
|
706
831
|
position: name,
|
|
707
832
|
arrowType: type,
|
|
708
833
|
doc: config.argDocs?.[name],
|
|
834
|
+
...constraintSpecFields(
|
|
835
|
+
config.argConstraints?.[name],
|
|
836
|
+
config.argDefaults?.[name],
|
|
837
|
+
),
|
|
709
838
|
});
|
|
710
839
|
}
|
|
711
840
|
}
|
|
@@ -720,6 +849,7 @@ export function defineRowTransformFunction<
|
|
|
720
849
|
examples: config.examples,
|
|
721
850
|
categories: config.categories,
|
|
722
851
|
tags: config.tags,
|
|
852
|
+
parameterDefaultValues: config.parameterDefaultValues,
|
|
723
853
|
maxWorkers: config.maxWorkers,
|
|
724
854
|
requiredSettings: config.requiredSettings,
|
|
725
855
|
requiredSecrets: config.requiredSecrets,
|
|
@@ -740,6 +870,8 @@ export function defineRowTransformFunction<
|
|
|
740
870
|
: undefined;
|
|
741
871
|
let val = request.arguments.get(spec.position, defaultVal ?? null);
|
|
742
872
|
val = narrowArgValue(val);
|
|
873
|
+
const constraints = config.argConstraints?.[spec.name];
|
|
874
|
+
if (constraints) validateConstConstraints(spec.name, constraints, val);
|
|
743
875
|
args[spec.name] = val;
|
|
744
876
|
}
|
|
745
877
|
return args as TArgs;
|
|
@@ -759,6 +891,7 @@ export function defineRowTransformFunction<
|
|
|
759
891
|
bindCall: request,
|
|
760
892
|
settings,
|
|
761
893
|
secrets,
|
|
894
|
+
argumentNames: request.argument_names ?? null,
|
|
762
895
|
});
|
|
763
896
|
return {
|
|
764
897
|
output_schema: result.outputSchema,
|
|
@@ -799,9 +932,14 @@ export function defineRowTransformFunction<
|
|
|
799
932
|
? projectSchema(projIds, request.output_schema)
|
|
800
933
|
: request.output_schema;
|
|
801
934
|
|
|
802
|
-
const joinKeysLookup = buildJoinKeysLookup(request.join_keys);
|
|
803
935
|
const pushdownFilters = request.pushdown_filters
|
|
804
|
-
? deserializeFilters(request.pushdown_filters,
|
|
936
|
+
? deserializeFilters(request.pushdown_filters, {
|
|
937
|
+
outputSchema: request.output_schema,
|
|
938
|
+
joinKeyBatches: request.join_keys,
|
|
939
|
+
extensionFunctions: meta.additionalFilterFunctions,
|
|
940
|
+
runtimeAlgorithms: meta.runtimeFilterAlgorithms,
|
|
941
|
+
evaluationContexts: meta.filterEvaluationContexts,
|
|
942
|
+
})
|
|
805
943
|
: undefined;
|
|
806
944
|
|
|
807
945
|
const boundStorage = new BoundStorage(
|
|
@@ -828,9 +966,26 @@ export function defineRowTransformFunction<
|
|
|
828
966
|
);
|
|
829
967
|
}
|
|
830
968
|
|
|
969
|
+
if (request.bind_call.input_schema == null) {
|
|
970
|
+
// A caller drove this function via table_function() (or the bare
|
|
971
|
+
// producer path) instead of table_in_out_function(input=...) -- a
|
|
972
|
+
// blended row-transform function's positional args ARE its per-row
|
|
973
|
+
// input columns, so it cannot run with no input stream at all. Left
|
|
974
|
+
// unguarded, this doesn't fail here -- it silently degrades to an
|
|
975
|
+
// empty inputSchema, which disables the transport layer's own
|
|
976
|
+
// schema-conformance check downstream and leaves both sides waiting
|
|
977
|
+
// on each other forever: confirmed live, a non-terminating exchange/
|
|
978
|
+
// continuation loop against a real deployed worker, not an error.
|
|
979
|
+
throw new Error(
|
|
980
|
+
`${config.name}: is a table-in-out function (its positional args are ` +
|
|
981
|
+
`per-row input columns) but no input schema was supplied -- call it ` +
|
|
982
|
+
`via table_in_out_function(input=...), not table_function().`,
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
|
|
831
986
|
return {
|
|
832
987
|
outputSchema,
|
|
833
|
-
inputSchema: request.bind_call.input_schema
|
|
988
|
+
inputSchema: request.bind_call.input_schema,
|
|
834
989
|
exchangeInit: () => ({ state: null, processParams }),
|
|
835
990
|
exchangeFn: async (
|
|
836
991
|
eState: { state: null; processParams: RowTransformProcessParams<TArgs> },
|
|
@@ -838,12 +993,14 @@ export function defineRowTransformFunction<
|
|
|
838
993
|
out: OutputCollector,
|
|
839
994
|
) => {
|
|
840
995
|
applyRevalidationValidators(eState.processParams, input);
|
|
996
|
+
applyDynamicFilterUpdate(eState.processParams, input);
|
|
841
997
|
let wrappedOut: OutputCollector = makeMetadataBakingCollector(out);
|
|
842
998
|
if (projIds) {
|
|
843
999
|
wrappedOut = makeSchemaReconcilingCollector(wrappedOut, outputSchema);
|
|
844
1000
|
}
|
|
845
|
-
|
|
846
|
-
|
|
1001
|
+
const currentFilters = eState.processParams.pushdownFilters;
|
|
1002
|
+
if (config.autoApplyFilters && currentFilters) {
|
|
1003
|
+
wrappedOut = new FilteringOutputCollector(wrappedOut, currentFilters) as unknown as OutputCollector;
|
|
847
1004
|
}
|
|
848
1005
|
await config.process(eState.processParams, input, wrappedOut);
|
|
849
1006
|
},
|
package/src/functions/table.ts
CHANGED
|
@@ -43,7 +43,6 @@ import {
|
|
|
43
43
|
import { assertArrowType, assertArrowTypes } from "../arguments/argument-spec.js";
|
|
44
44
|
import { batchToScalarDict, batchToSecretDict, projectSchema, safeNumber } from "../util/arrow/index.js";
|
|
45
45
|
import {
|
|
46
|
-
buildJoinKeysLookup,
|
|
47
46
|
deserializeFilters,
|
|
48
47
|
FilteringOutputCollector,
|
|
49
48
|
type PushdownFilters,
|
|
@@ -62,9 +61,16 @@ import { CACHE_IF_MODIFIED_SINCE_KEY, CACHE_IF_NONE_MATCH_KEY } from "../cache-c
|
|
|
62
61
|
// Base64-decode a string into raw bytes. Used to unpack the dynamic filter
|
|
63
62
|
// update DuckDB attaches to each tick batch's custom metadata.
|
|
64
63
|
function base64Decode(s: string): Uint8Array {
|
|
64
|
+
if (new TextEncoder().encode(s).byteLength > (24 << 20)) {
|
|
65
|
+
throw new Error("base64 dynamic filter metadata exceeds the encoded-size limit");
|
|
66
|
+
}
|
|
67
|
+
if (s.length % 4 !== 0 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(s)) {
|
|
68
|
+
throw new Error("dynamic filter metadata is not valid base64");
|
|
69
|
+
}
|
|
65
70
|
const bin = (globalThis as any).atob ? (globalThis as any).atob(s) : Buffer.from(s, "base64").toString("binary");
|
|
66
71
|
const out = new Uint8Array(bin.length);
|
|
67
72
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
73
|
+
if (out.byteLength > (17 << 20)) throw new Error("dynamic filter IPC payload exceeds the encoded-size limit");
|
|
68
74
|
return out;
|
|
69
75
|
}
|
|
70
76
|
|
|
@@ -140,6 +146,8 @@ export interface TableBindParams<TArgs = Record<string, any>> {
|
|
|
140
146
|
settings: Record<string, any>;
|
|
141
147
|
secrets: Record<string, Record<string, any>>;
|
|
142
148
|
resolvedSecretsProvided: boolean;
|
|
149
|
+
/** Resolved names aligned with the complete logical argument order. */
|
|
150
|
+
argumentNames?: (string | null)[] | null;
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
export interface TableProcessParams<TArgs = Record<string, any>> {
|
|
@@ -199,6 +207,8 @@ export interface TableFunctionConfig<
|
|
|
199
207
|
argDocs?: Record<string, string>;
|
|
200
208
|
/** Argument defaults */
|
|
201
209
|
argDefaults?: Record<string, any>;
|
|
210
|
+
/** Authoritative typed defaults: one row, defaulted parameters only. */
|
|
211
|
+
parameterDefaultValues?: VgiBatch | null;
|
|
202
212
|
/**
|
|
203
213
|
* Per-argument discovery constraints (choices / ge / le / gt / lt / pattern),
|
|
204
214
|
* keyed by argument name. Surfaced via `vgi_function_arguments()` for agent
|
|
@@ -310,7 +320,10 @@ export interface TableFunctionConfig<
|
|
|
310
320
|
/** Opt in to DuckDB's late-materialization SEMI-join rewrite; requires a
|
|
311
321
|
* UNIQUE, snapshot-stable rowid column. FunctionInfo.late_materialization. */
|
|
312
322
|
lateMaterialization?: boolean;
|
|
313
|
-
|
|
323
|
+
filterSemanticProfiles?: string[];
|
|
324
|
+
additionalFilterFunctions?: import("./types.js").FilterFunctionCapability[];
|
|
325
|
+
runtimeFilterAlgorithms?: import("./types.js").FilterFunctionCapability[];
|
|
326
|
+
filterEvaluationContexts?: import("./types.js").EvaluationContextCapability[];
|
|
314
327
|
autoApplyFilters?: boolean;
|
|
315
328
|
stability?: FunctionStability;
|
|
316
329
|
examples?: FunctionExample[];
|
|
@@ -373,7 +386,10 @@ export function defineTableFunction<
|
|
|
373
386
|
isAnyType: isAny,
|
|
374
387
|
isVarargs: varargsSet.has(name),
|
|
375
388
|
doc: config.argDocs?.[name],
|
|
376
|
-
...constraintSpecFields(
|
|
389
|
+
...constraintSpecFields(
|
|
390
|
+
config.argConstraints?.[name],
|
|
391
|
+
config.argDefaults?.[name],
|
|
392
|
+
),
|
|
377
393
|
});
|
|
378
394
|
}
|
|
379
395
|
}
|
|
@@ -386,11 +402,15 @@ export function defineTableFunction<
|
|
|
386
402
|
filterPushdown: config.filterPushdown,
|
|
387
403
|
samplingPushdown: config.samplingPushdown,
|
|
388
404
|
lateMaterialization: config.lateMaterialization,
|
|
389
|
-
|
|
405
|
+
filterSemanticProfiles: config.filterSemanticProfiles,
|
|
406
|
+
additionalFilterFunctions: config.additionalFilterFunctions,
|
|
407
|
+
runtimeFilterAlgorithms: config.runtimeFilterAlgorithms,
|
|
408
|
+
filterEvaluationContexts: config.filterEvaluationContexts,
|
|
390
409
|
autoApplyFilters: config.autoApplyFilters,
|
|
391
410
|
examples: config.examples,
|
|
392
411
|
categories: config.categories,
|
|
393
412
|
tags: config.tags,
|
|
413
|
+
parameterDefaultValues: config.parameterDefaultValues,
|
|
394
414
|
maxWorkers: config.maxWorkers,
|
|
395
415
|
requiredSettings: config.requiredSettings,
|
|
396
416
|
requiredSecrets: config.requiredSecrets,
|
|
@@ -442,6 +462,7 @@ export function defineTableFunction<
|
|
|
442
462
|
const result = await config.onBind({
|
|
443
463
|
args, bindCall: request, settings, secrets,
|
|
444
464
|
resolvedSecretsProvided: request.resolved_secrets_provided ?? false,
|
|
465
|
+
argumentNames: request.argument_names ?? null,
|
|
445
466
|
});
|
|
446
467
|
return {
|
|
447
468
|
output_schema: result.outputSchema,
|
|
@@ -525,9 +546,15 @@ export function defineTableFunction<
|
|
|
525
546
|
// Deserialize pushdown filters. Pass a join-keys column lookup so that
|
|
526
547
|
// filters DuckDB promoted to join_keys (IN/OR lists, etc.) are
|
|
527
548
|
// materialized as InFilters rather than silently dropped.
|
|
528
|
-
const
|
|
549
|
+
const filterOptions = {
|
|
550
|
+
outputSchema: request.output_schema,
|
|
551
|
+
joinKeyBatches: request.join_keys,
|
|
552
|
+
extensionFunctions: meta.additionalFilterFunctions,
|
|
553
|
+
runtimeAlgorithms: meta.runtimeFilterAlgorithms,
|
|
554
|
+
evaluationContexts: meta.filterEvaluationContexts,
|
|
555
|
+
};
|
|
529
556
|
const pushdownFilters = request.pushdown_filters
|
|
530
|
-
? deserializeFilters(request.pushdown_filters,
|
|
557
|
+
? deserializeFilters(request.pushdown_filters, filterOptions)
|
|
531
558
|
: undefined;
|
|
532
559
|
|
|
533
560
|
const boundStorage = new BoundStorage(globalStorage, response.execution_id);
|
|
@@ -546,6 +573,20 @@ export function defineTableFunction<
|
|
|
546
573
|
atValue: request.bind_call.at_value ?? undefined,
|
|
547
574
|
};
|
|
548
575
|
|
|
576
|
+
if (request.phase != null) {
|
|
577
|
+
// Mirror image of the table-in-out guard: this is a plain producer
|
|
578
|
+
// (no input stream), but the caller sent a table-in-out init phase --
|
|
579
|
+
// i.e. drove it via table_in_out_function() instead of
|
|
580
|
+
// table_function(). Reject immediately rather than silently ignoring
|
|
581
|
+
// the phase/input stream and running as an ordinary producer while
|
|
582
|
+
// the caller is left feeding batches nobody reads.
|
|
583
|
+
throw new Error(
|
|
584
|
+
`${config.name}: is a plain table function (it takes no input row ` +
|
|
585
|
+
`stream) but was called with an init phase set -- call it via ` +
|
|
586
|
+
`table_function(), not table_in_out_function().`,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
549
590
|
const state = config.initialState
|
|
550
591
|
? config.initialState(processParams)
|
|
551
592
|
: (null as TState);
|
|
@@ -570,23 +611,20 @@ export function defineTableFunction<
|
|
|
570
611
|
if (ifModifiedSince !== undefined) pState.processParams.ifModifiedSince = ifModifiedSince;
|
|
571
612
|
|
|
572
613
|
// Dynamic filter pushdown: DuckDB's Top-N optimizer tightens filters
|
|
573
|
-
// between ticks and serializes
|
|
614
|
+
// between ticks and serializes an atomic v2 delta into the tick
|
|
574
615
|
// batch's custom metadata under `vgi_pushdown_filters` (base64 of a
|
|
575
|
-
// filter IPC stream).
|
|
576
|
-
//
|
|
616
|
+
// filter IPC stream). Apply it to the prior snapshot so process()
|
|
617
|
+
// sees the revised advisory predicate state.
|
|
577
618
|
const encoded = tickMetadata.get("vgi_pushdown_filters");
|
|
578
619
|
if (!encoded) return;
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
// Malformed dynamic-filter update: keep the previous filter. Not
|
|
588
|
-
// fatal — this is a best-effort optimization hint from DuckDB.
|
|
589
|
-
}
|
|
620
|
+
const current = pState.processParams.pushdownFilters;
|
|
621
|
+
if (!current) throw new Error("dynamic filter delta received without an initial snapshot");
|
|
622
|
+
const bytes = base64Decode(encoded);
|
|
623
|
+
const filterBatch = deserializeFilterBatch(bytes);
|
|
624
|
+
if (!filterBatch) throw new Error("dynamic filter metadata must contain one RecordBatch");
|
|
625
|
+
// Assignment happens only after complete validation, so a rejected
|
|
626
|
+
// delta leaves the immutable prior state intact.
|
|
627
|
+
pState.processParams.pushdownFilters = current.applyDelta(filterBatch);
|
|
590
628
|
},
|
|
591
629
|
producerFn: async (
|
|
592
630
|
pState: { state: TState; processParams: TableProcessParams<TArgs> },
|
package/src/functions/types.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { OutputCollector } from "@query-farm/vgi-rpc";
|
|
|
6
6
|
import type {
|
|
7
7
|
FunctionStability,
|
|
8
8
|
NullHandling,
|
|
9
|
+
ArgumentMonotonicity,
|
|
9
10
|
OrderPreservation,
|
|
10
11
|
OrderDependence,
|
|
11
12
|
DistinctDependence,
|
|
@@ -34,14 +35,33 @@ export interface FunctionExample {
|
|
|
34
35
|
expectedOutput?: string;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
export interface FilterFunctionCapability {
|
|
39
|
+
namespace: string;
|
|
40
|
+
name: string;
|
|
41
|
+
version: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface EvaluationContextCapability {
|
|
45
|
+
profile: string;
|
|
46
|
+
providerFingerprint?: string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
export interface FunctionMeta {
|
|
38
50
|
name: string;
|
|
39
51
|
description?: string;
|
|
40
52
|
stability?: FunctionStability;
|
|
41
53
|
nullHandling?: NullHandling;
|
|
54
|
+
/** Scalar-only claims in argument declaration order. A vararg is one slot. */
|
|
55
|
+
argumentMonotonicity?: ArgumentMonotonicity[] | null;
|
|
42
56
|
examples?: FunctionExample[];
|
|
43
57
|
categories?: string[];
|
|
44
58
|
tags?: Record<string, string>;
|
|
59
|
+
/**
|
|
60
|
+
* Authoritative typed parameter defaults. The batch must contain one row
|
|
61
|
+
* and only defaulted parameters, in signature order; a present null is an
|
|
62
|
+
* explicit null default. `vgi_default` field metadata remains discovery-only.
|
|
63
|
+
*/
|
|
64
|
+
parameterDefaultValues?: VgiBatch | null;
|
|
45
65
|
projectionPushdown?: boolean;
|
|
46
66
|
filterPushdown?: boolean;
|
|
47
67
|
samplingPushdown?: boolean;
|
|
@@ -54,7 +74,10 @@ export interface FunctionMeta {
|
|
|
54
74
|
* for tables whose worker also guarantees a UNIQUE, snapshot-stable rowid.
|
|
55
75
|
*/
|
|
56
76
|
lateMaterialization?: boolean;
|
|
57
|
-
|
|
77
|
+
filterSemanticProfiles?: string[];
|
|
78
|
+
additionalFilterFunctions?: FilterFunctionCapability[];
|
|
79
|
+
runtimeFilterAlgorithms?: FilterFunctionCapability[];
|
|
80
|
+
filterEvaluationContexts?: EvaluationContextCapability[];
|
|
58
81
|
autoApplyFilters?: boolean;
|
|
59
82
|
preservesOrder?: OrderPreservation;
|
|
60
83
|
maxWorkers?: number;
|