@query-farm/vgi 0.28.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 +52 -8
- package/dist/arguments/argument-spec.d.ts +57 -2
- 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/composite.d.ts +13 -13
- package/dist/catalog/descriptors.d.ts +7 -3
- package/dist/catalog/interface.d.ts +108 -38
- package/dist/catalog/read-only.d.ts +32 -15
- package/dist/client/client.d.ts +37 -30
- package/dist/client/types.d.ts +2 -0
- package/dist/client-entry.d.ts +1 -1
- package/dist/client-entry.js +791 -447
- package/dist/client-entry.js.map +18 -17
- 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 +72 -2
- package/dist/functions/types.d.ts +60 -2
- package/dist/generated/vgi-client.d.ts +114 -26
- package/dist/generated/vgi-protocol-schemas.d.ts +34 -0
- package/dist/http/fetch.d.ts +16 -0
- package/dist/index.core.d.ts +4 -3
- package/dist/index.js +3396 -2064
- package/dist/index.js.map +55 -49
- package/dist/metadata/types.d.ts +15 -2
- package/dist/protocol/handlers/catalog/shared.d.ts +0 -8
- package/dist/protocol/handlers/function.d.ts +11 -0
- package/dist/protocol/handlers/shared.d.ts +3 -1
- 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 +36 -3
- package/dist/request-auth.d.ts +28 -0
- package/dist/schema-path.d.ts +7 -0
- package/dist/serve-entry.d.ts +25 -0
- package/dist/serve-entry.js +1129 -569
- package/dist/serve-entry.js.map +36 -32
- package/dist/split-token.d.ts +88 -0
- package/dist/types.d.ts +8 -0
- package/dist/worker-cf-entry.js +2928 -1689
- package/dist/worker-cf-entry.js.map +51 -44
- package/dist/worker.d.ts +1 -1
- package/package.json +3 -3
- package/src/arguments/argument-spec.ts +93 -4
- 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/composite.ts +26 -26
- package/src/catalog/descriptors.ts +12 -3
- package/src/catalog/interface.ts +217 -72
- package/src/catalog/read-only.ts +141 -84
- package/src/client/client.ts +97 -73
- 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 +12 -1
- package/src/functions/copy-from.ts +2 -1
- package/src/functions/copy-to.ts +2 -1
- package/src/functions/registry.ts +24 -22
- package/src/functions/scalar.ts +37 -0
- package/src/functions/table-buffering.ts +48 -6
- package/src/functions/table-in-out.ts +194 -33
- package/src/functions/table.ts +187 -22
- package/src/functions/types.ts +64 -1
- package/src/generated/vgi-client.ts +122 -28
- package/src/generated/vgi-protocol-schemas.ts +455 -58
- package/src/http/fetch.ts +129 -2
- package/src/index.core.ts +4 -2
- 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 +23 -12
- package/src/protocol/handlers/catalog/admin.ts +47 -54
- package/src/protocol/handlers/catalog/index_methods.ts +6 -13
- package/src/protocol/handlers/catalog/macro.ts +12 -20
- package/src/protocol/handlers/catalog/shared.ts +12 -57
- package/src/protocol/handlers/catalog/table.ts +49 -112
- package/src/protocol/handlers/catalog/view.ts +18 -22
- package/src/protocol/handlers/function.ts +311 -18
- package/src/protocol/handlers/shared.ts +12 -3
- package/src/protocol/handlers/table-buffering.ts +11 -8
- package/src/protocol/serializers/bind.ts +69 -67
- package/src/protocol/serializers/cardinality.ts +7 -7
- package/src/protocol/serializers/init.ts +52 -10
- package/src/protocol/serializers/splits.ts +221 -0
- package/src/protocol/state-serializer.ts +55 -1
- package/src/protocol/types.ts +36 -3
- package/src/request-auth.ts +73 -0
- package/src/schema-path.ts +25 -0
- package/src/serve-entry.ts +69 -2
- package/src/split-token.ts +297 -0
- package/src/types.ts +9 -0
- package/src/worker-cf-entry.ts +2 -1
- package/src/worker.ts +79 -14
|
@@ -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,19 +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";
|
|
29
|
+
import { batchToScalarDict, batchToSecretDict, projectSchema, projectBatch, emptyBatch, safeNumber } from "../util/arrow/index.js";
|
|
23
30
|
import { CACHE_IF_MODIFIED_SINCE_KEY, CACHE_IF_NONE_MATCH_KEY } from "../cache-control.js";
|
|
24
31
|
import {
|
|
25
|
-
buildJoinKeysLookup,
|
|
26
32
|
deserializeFilters,
|
|
27
33
|
FilteringOutputCollector,
|
|
28
34
|
type PushdownFilters,
|
|
29
35
|
} from "../filter-pushdown/index.js";
|
|
30
36
|
import { FunctionStability } from "../types.js";
|
|
31
|
-
import { BoundStorage, storage as defaultStorage } from "./storage.js";
|
|
37
|
+
import { BoundStorage, storage as defaultStorage, FrameworkNS } from "./storage.js";
|
|
32
38
|
import { serializeUserState, deserializeUserState } from "../protocol/state-serializer.js";
|
|
33
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
|
+
|
|
34
48
|
// ============================================================================
|
|
35
49
|
// Table In-Out parameter bundles (reuse table function's)
|
|
36
50
|
// ============================================================================
|
|
@@ -40,6 +54,8 @@ export interface TableInOutBindParams<TArgs = Record<string, any>> {
|
|
|
40
54
|
bindCall: BindRequest;
|
|
41
55
|
settings: Record<string, any>;
|
|
42
56
|
secrets: Record<string, Record<string, any>>;
|
|
57
|
+
/** Resolved names aligned with the complete logical argument order. */
|
|
58
|
+
argumentNames?: (string | null)[] | null;
|
|
43
59
|
}
|
|
44
60
|
|
|
45
61
|
export interface TableInOutProcessParams<TArgs = Record<string, any>> {
|
|
@@ -91,6 +107,12 @@ export interface TableInOutConfig<
|
|
|
91
107
|
namedArgs?: Record<string, VgiDataType>;
|
|
92
108
|
/** Argument defaults */
|
|
93
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>;
|
|
94
116
|
/** Bind: default passes through input schema. May be async. */
|
|
95
117
|
onBind?: (params: TableInOutBindParams<TArgs>) =>
|
|
96
118
|
| { outputSchema: VgiSchema; opaqueData?: Uint8Array }
|
|
@@ -132,6 +154,8 @@ export function defineTableInOutFunction<
|
|
|
132
154
|
TArgs = Record<string, any>,
|
|
133
155
|
TState = null,
|
|
134
156
|
>(config: TableInOutConfig<TArgs, TState>): VgiFunction {
|
|
157
|
+
assertArrowTypes((config as any).args, `defineTableInOutFunction("${config.name}"): args`);
|
|
158
|
+
|
|
135
159
|
|
|
136
160
|
// Build argument specs
|
|
137
161
|
const specs: ArgumentSpec[] = [];
|
|
@@ -144,6 +168,11 @@ export function defineTableInOutFunction<
|
|
|
144
168
|
name,
|
|
145
169
|
position: posIdx++,
|
|
146
170
|
arrowType: type,
|
|
171
|
+
doc: config.argDocs?.[name],
|
|
172
|
+
...constraintSpecFields(
|
|
173
|
+
config.argConstraints?.[name],
|
|
174
|
+
config.argDefaults?.[name],
|
|
175
|
+
),
|
|
147
176
|
});
|
|
148
177
|
}
|
|
149
178
|
}
|
|
@@ -162,6 +191,11 @@ export function defineTableInOutFunction<
|
|
|
162
191
|
name,
|
|
163
192
|
position: name, // string position = named arg
|
|
164
193
|
arrowType: type,
|
|
194
|
+
doc: config.argDocs?.[name],
|
|
195
|
+
...constraintSpecFields(
|
|
196
|
+
config.argConstraints?.[name],
|
|
197
|
+
config.argDefaults?.[name],
|
|
198
|
+
),
|
|
165
199
|
});
|
|
166
200
|
}
|
|
167
201
|
}
|
|
@@ -176,6 +210,7 @@ export function defineTableInOutFunction<
|
|
|
176
210
|
examples: config.examples,
|
|
177
211
|
categories: config.categories,
|
|
178
212
|
tags: config.tags,
|
|
213
|
+
parameterDefaultValues: config.parameterDefaultValues,
|
|
179
214
|
maxWorkers: config.maxWorkers,
|
|
180
215
|
requiredSettings: config.requiredSettings,
|
|
181
216
|
requiredSecrets: config.requiredSecrets,
|
|
@@ -192,8 +227,11 @@ export function defineTableInOutFunction<
|
|
|
192
227
|
: undefined;
|
|
193
228
|
// For named args, position is a string (the arg name)
|
|
194
229
|
let val = request.arguments.get(spec.position, defaultVal);
|
|
195
|
-
// Arrow Int64
|
|
196
|
-
|
|
230
|
+
// Arrow Int64 arrives as bigint. Narrow when lossless, keep the
|
|
231
|
+
// bigint when not — see narrowArgValue.
|
|
232
|
+
val = narrowArgValue(val);
|
|
233
|
+
const constraints = config.argConstraints?.[spec.name];
|
|
234
|
+
if (constraints) validateConstConstraints(spec.name, constraints, val);
|
|
197
235
|
args[spec.name] = val;
|
|
198
236
|
}
|
|
199
237
|
return args as TArgs;
|
|
@@ -214,6 +252,7 @@ export function defineTableInOutFunction<
|
|
|
214
252
|
bindCall: request,
|
|
215
253
|
settings,
|
|
216
254
|
secrets,
|
|
255
|
+
argumentNames: request.argument_names ?? null,
|
|
217
256
|
});
|
|
218
257
|
return {
|
|
219
258
|
output_schema: result.outputSchema,
|
|
@@ -276,9 +315,14 @@ export function defineTableInOutFunction<
|
|
|
276
315
|
// Deserialize pushdown filters. Pass a join-keys column lookup so that
|
|
277
316
|
// filters DuckDB promoted to join_keys (IN/OR lists, etc.) are
|
|
278
317
|
// materialized as InFilters rather than silently dropped.
|
|
279
|
-
const joinKeysLookup = buildJoinKeysLookup(request.join_keys);
|
|
280
318
|
const pushdownFilters = request.pushdown_filters
|
|
281
|
-
? 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
|
+
})
|
|
282
326
|
: undefined;
|
|
283
327
|
|
|
284
328
|
// Create BoundStorage for cross-phase/cross-worker data sharing
|
|
@@ -302,18 +346,41 @@ export function defineTableInOutFunction<
|
|
|
302
346
|
const phase = request.phase;
|
|
303
347
|
|
|
304
348
|
if (phase === TableInOutPhase.FINALIZE) {
|
|
305
|
-
// FINALIZE phase: producer mode
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
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;
|
|
309
376
|
return {
|
|
310
377
|
outputSchema,
|
|
311
|
-
producerInit: () => ({ state: {
|
|
378
|
+
producerInit: () => ({ state: { cursor: -1, materialized: false } }),
|
|
312
379
|
producerFn: async (
|
|
313
|
-
pState: { state: {
|
|
380
|
+
pState: { state: { cursor: number; materialized: boolean } },
|
|
314
381
|
out: OutputCollector
|
|
315
382
|
) => {
|
|
316
|
-
if (pState.
|
|
383
|
+
if (!pState.state.materialized) {
|
|
317
384
|
const finalizeStates: TState[] = [];
|
|
318
385
|
if (accumulatedState != null) {
|
|
319
386
|
finalizeStates.push(accumulatedState as TState);
|
|
@@ -332,24 +399,55 @@ export function defineTableInOutFunction<
|
|
|
332
399
|
: (null as TState),
|
|
333
400
|
);
|
|
334
401
|
}
|
|
335
|
-
|
|
402
|
+
const batches: VgiBatch[] = config.finalize
|
|
336
403
|
? await config.finalize(processParams, finalizeStates)
|
|
337
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;
|
|
338
416
|
}
|
|
339
|
-
|
|
417
|
+
const rows = await boundStorage.stateLogScan(
|
|
418
|
+
FrameworkNS.STREAMING_FINALIZE,
|
|
419
|
+
finalizeLogKey,
|
|
420
|
+
pState.state.cursor,
|
|
421
|
+
1,
|
|
422
|
+
);
|
|
423
|
+
if (rows.length === 0) {
|
|
340
424
|
out.finish();
|
|
341
425
|
return;
|
|
342
426
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
out.finish();
|
|
347
|
-
}
|
|
427
|
+
const [logId, bytes] = rows[0];
|
|
428
|
+
out.emit(deserializeBatch(bytes) as unknown as VgiBatch);
|
|
429
|
+
pState.state.cursor = logId;
|
|
348
430
|
},
|
|
349
431
|
};
|
|
350
432
|
}
|
|
351
433
|
|
|
352
|
-
// 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
|
+
|
|
353
451
|
const state = config.initialState
|
|
354
452
|
? config.initialState(processParams)
|
|
355
453
|
: (null as TState);
|
|
@@ -368,7 +466,7 @@ export function defineTableInOutFunction<
|
|
|
368
466
|
|
|
369
467
|
return {
|
|
370
468
|
outputSchema,
|
|
371
|
-
inputSchema: request.bind_call.input_schema
|
|
469
|
+
inputSchema: request.bind_call.input_schema,
|
|
372
470
|
exchangeInit: () => ({ state, processParams }),
|
|
373
471
|
exchangeFn: async (
|
|
374
472
|
eState: {
|
|
@@ -382,6 +480,7 @@ export function defineTableInOutFunction<
|
|
|
382
480
|
// custom metadata (attached by the C++ WriteInputBatch), so process()
|
|
383
481
|
// can answer a 0-row `notModified` batch instead of recomputing.
|
|
384
482
|
applyRevalidationValidators(eState.processParams, input);
|
|
483
|
+
applyDynamicFilterUpdate(eState.processParams, input);
|
|
385
484
|
// Bake emit metadata onto the batch (0-row HTTP survival), then
|
|
386
485
|
// reconcile emitted batches to the (possibly projected) output schema
|
|
387
486
|
// by name — a process() may emit its full declared schema and let the
|
|
@@ -390,8 +489,9 @@ export function defineTableInOutFunction<
|
|
|
390
489
|
if (projIds) {
|
|
391
490
|
wrappedOut = makeSchemaReconcilingCollector(wrappedOut, outputSchema);
|
|
392
491
|
}
|
|
393
|
-
|
|
394
|
-
|
|
492
|
+
const currentFilters = eState.processParams.pushdownFilters;
|
|
493
|
+
if (config.autoApplyFilters && currentFilters) {
|
|
494
|
+
wrappedOut = new FilteringOutputCollector(wrappedOut, currentFilters) as unknown as OutputCollector;
|
|
395
495
|
}
|
|
396
496
|
await processFn(eState.processParams, eState.state, input, wrappedOut);
|
|
397
497
|
|
|
@@ -429,6 +529,29 @@ function applyRevalidationValidators(
|
|
|
429
529
|
params.ifModifiedSince = md?.get(CACHE_IF_MODIFIED_SINCE_KEY);
|
|
430
530
|
}
|
|
431
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
|
+
|
|
432
555
|
/**
|
|
433
556
|
* Wrap an OutputCollector so a pre-built batch emitted WITH metadata carries
|
|
434
557
|
* that metadata on the batch object itself (merged over any metadata the
|
|
@@ -615,8 +738,12 @@ export interface RowTransformConfig<TArgs = Record<string, any>> {
|
|
|
615
738
|
/** Named (string-position) args stay bind-time scalars on `params.args`. */
|
|
616
739
|
namedArgs?: Record<string, VgiDataType>;
|
|
617
740
|
argDefaults?: Record<string, any>;
|
|
741
|
+
/** Authoritative typed defaults: one row, defaulted parameters only. */
|
|
742
|
+
parameterDefaultValues?: VgiBatch | null;
|
|
618
743
|
/** Per-argument descriptions keyed by arg name (surfaced as `vgi_doc`). */
|
|
619
744
|
argDocs?: Record<string, string>;
|
|
745
|
+
/** Discovery constraints, also enforced for named scalar arguments at bind. */
|
|
746
|
+
argConstraints?: Record<string, ArgumentConstraints>;
|
|
620
747
|
/** Bind: return the output schema. The input schema (the declared per-row
|
|
621
748
|
* columns, typed by the C++ bind) is on `params.bindCall.input_schema`. */
|
|
622
749
|
onBind: (params: TableInOutBindParams<TArgs>) =>
|
|
@@ -683,6 +810,7 @@ export function defineRowTransformFunction<
|
|
|
683
810
|
position: posIdx++,
|
|
684
811
|
arrowType: type,
|
|
685
812
|
doc: config.argDocs?.[name],
|
|
813
|
+
...constraintSpecFields(config.argConstraints?.[name]),
|
|
686
814
|
});
|
|
687
815
|
}
|
|
688
816
|
}
|
|
@@ -693,6 +821,7 @@ export function defineRowTransformFunction<
|
|
|
693
821
|
arrowType: config.varargs.type,
|
|
694
822
|
isVarargs: true,
|
|
695
823
|
doc: config.varargs.doc ?? config.argDocs?.[config.varargs.name],
|
|
824
|
+
...constraintSpecFields(config.argConstraints?.[config.varargs.name]),
|
|
696
825
|
});
|
|
697
826
|
}
|
|
698
827
|
if (config.namedArgs) {
|
|
@@ -702,6 +831,10 @@ export function defineRowTransformFunction<
|
|
|
702
831
|
position: name,
|
|
703
832
|
arrowType: type,
|
|
704
833
|
doc: config.argDocs?.[name],
|
|
834
|
+
...constraintSpecFields(
|
|
835
|
+
config.argConstraints?.[name],
|
|
836
|
+
config.argDefaults?.[name],
|
|
837
|
+
),
|
|
705
838
|
});
|
|
706
839
|
}
|
|
707
840
|
}
|
|
@@ -716,6 +849,7 @@ export function defineRowTransformFunction<
|
|
|
716
849
|
examples: config.examples,
|
|
717
850
|
categories: config.categories,
|
|
718
851
|
tags: config.tags,
|
|
852
|
+
parameterDefaultValues: config.parameterDefaultValues,
|
|
719
853
|
maxWorkers: config.maxWorkers,
|
|
720
854
|
requiredSettings: config.requiredSettings,
|
|
721
855
|
requiredSecrets: config.requiredSecrets,
|
|
@@ -735,7 +869,9 @@ export function defineRowTransformFunction<
|
|
|
735
869
|
? config.argDefaults[spec.name]
|
|
736
870
|
: undefined;
|
|
737
871
|
let val = request.arguments.get(spec.position, defaultVal ?? null);
|
|
738
|
-
|
|
872
|
+
val = narrowArgValue(val);
|
|
873
|
+
const constraints = config.argConstraints?.[spec.name];
|
|
874
|
+
if (constraints) validateConstConstraints(spec.name, constraints, val);
|
|
739
875
|
args[spec.name] = val;
|
|
740
876
|
}
|
|
741
877
|
return args as TArgs;
|
|
@@ -755,6 +891,7 @@ export function defineRowTransformFunction<
|
|
|
755
891
|
bindCall: request,
|
|
756
892
|
settings,
|
|
757
893
|
secrets,
|
|
894
|
+
argumentNames: request.argument_names ?? null,
|
|
758
895
|
});
|
|
759
896
|
return {
|
|
760
897
|
output_schema: result.outputSchema,
|
|
@@ -795,9 +932,14 @@ export function defineRowTransformFunction<
|
|
|
795
932
|
? projectSchema(projIds, request.output_schema)
|
|
796
933
|
: request.output_schema;
|
|
797
934
|
|
|
798
|
-
const joinKeysLookup = buildJoinKeysLookup(request.join_keys);
|
|
799
935
|
const pushdownFilters = request.pushdown_filters
|
|
800
|
-
? 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
|
+
})
|
|
801
943
|
: undefined;
|
|
802
944
|
|
|
803
945
|
const boundStorage = new BoundStorage(
|
|
@@ -824,9 +966,26 @@ export function defineRowTransformFunction<
|
|
|
824
966
|
);
|
|
825
967
|
}
|
|
826
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
|
+
|
|
827
986
|
return {
|
|
828
987
|
outputSchema,
|
|
829
|
-
inputSchema: request.bind_call.input_schema
|
|
988
|
+
inputSchema: request.bind_call.input_schema,
|
|
830
989
|
exchangeInit: () => ({ state: null, processParams }),
|
|
831
990
|
exchangeFn: async (
|
|
832
991
|
eState: { state: null; processParams: RowTransformProcessParams<TArgs> },
|
|
@@ -834,12 +993,14 @@ export function defineRowTransformFunction<
|
|
|
834
993
|
out: OutputCollector,
|
|
835
994
|
) => {
|
|
836
995
|
applyRevalidationValidators(eState.processParams, input);
|
|
996
|
+
applyDynamicFilterUpdate(eState.processParams, input);
|
|
837
997
|
let wrappedOut: OutputCollector = makeMetadataBakingCollector(out);
|
|
838
998
|
if (projIds) {
|
|
839
999
|
wrappedOut = makeSchemaReconcilingCollector(wrappedOut, outputSchema);
|
|
840
1000
|
}
|
|
841
|
-
|
|
842
|
-
|
|
1001
|
+
const currentFilters = eState.processParams.pushdownFilters;
|
|
1002
|
+
if (config.autoApplyFilters && currentFilters) {
|
|
1003
|
+
wrappedOut = new FilteringOutputCollector(wrappedOut, currentFilters) as unknown as OutputCollector;
|
|
843
1004
|
}
|
|
844
1005
|
await config.process(eState.processParams, input, wrappedOut);
|
|
845
1006
|
},
|