@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
package/src/functions/table.ts
CHANGED
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
import { codecFor } from "../arrow/codec/registry.js";
|
|
16
16
|
import type { OutputCollector } from "@query-farm/vgi-rpc";
|
|
17
17
|
import { DEFAULT_MAX_WORKERS } from "../types.js";
|
|
18
|
+
import type {
|
|
19
|
+
PlanResult,
|
|
20
|
+
TableFunctionPlanRequest,
|
|
21
|
+
} from "../protocol/serializers/splits.js";
|
|
18
22
|
import type {
|
|
19
23
|
BindRequest,
|
|
20
24
|
BindResponse,
|
|
@@ -34,10 +38,11 @@ import {
|
|
|
34
38
|
validateConstConstraints,
|
|
35
39
|
type ArgumentSpec,
|
|
36
40
|
type ArgumentConstraints,
|
|
41
|
+
narrowArgValue,
|
|
37
42
|
} from "../arguments/argument-spec.js";
|
|
43
|
+
import { assertArrowType, assertArrowTypes } from "../arguments/argument-spec.js";
|
|
38
44
|
import { batchToScalarDict, batchToSecretDict, projectSchema, safeNumber } from "../util/arrow/index.js";
|
|
39
45
|
import {
|
|
40
|
-
buildJoinKeysLookup,
|
|
41
46
|
deserializeFilters,
|
|
42
47
|
FilteringOutputCollector,
|
|
43
48
|
type PushdownFilters,
|
|
@@ -56,9 +61,16 @@ import { CACHE_IF_MODIFIED_SINCE_KEY, CACHE_IF_NONE_MATCH_KEY } from "../cache-c
|
|
|
56
61
|
// Base64-decode a string into raw bytes. Used to unpack the dynamic filter
|
|
57
62
|
// update DuckDB attaches to each tick batch's custom metadata.
|
|
58
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
|
+
}
|
|
59
70
|
const bin = (globalThis as any).atob ? (globalThis as any).atob(s) : Buffer.from(s, "base64").toString("binary");
|
|
60
71
|
const out = new Uint8Array(bin.length);
|
|
61
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");
|
|
62
74
|
return out;
|
|
63
75
|
}
|
|
64
76
|
|
|
@@ -134,6 +146,8 @@ export interface TableBindParams<TArgs = Record<string, any>> {
|
|
|
134
146
|
settings: Record<string, any>;
|
|
135
147
|
secrets: Record<string, Record<string, any>>;
|
|
136
148
|
resolvedSecretsProvided: boolean;
|
|
149
|
+
/** Resolved names aligned with the complete logical argument order. */
|
|
150
|
+
argumentNames?: (string | null)[] | null;
|
|
137
151
|
}
|
|
138
152
|
|
|
139
153
|
export interface TableProcessParams<TArgs = Record<string, any>> {
|
|
@@ -144,6 +158,15 @@ export interface TableProcessParams<TArgs = Record<string, any>> {
|
|
|
144
158
|
settings: Record<string, any>;
|
|
145
159
|
secrets: Record<string, Record<string, any>>;
|
|
146
160
|
pushdownFilters?: PushdownFilters;
|
|
161
|
+
/**
|
|
162
|
+
* The verified payloads of the splits this init claimed, in the order given,
|
|
163
|
+
* or undefined when this is not a split scan.
|
|
164
|
+
*
|
|
165
|
+
* Undefined is meaningfully different from an empty array: undefined means
|
|
166
|
+
* the client did not plan (a split-only function should fail loudly rather
|
|
167
|
+
* than answer the same query differently), empty means a claim of no work.
|
|
168
|
+
*/
|
|
169
|
+
splitPayloads?: Uint8Array[];
|
|
147
170
|
storage?: BoundStorage;
|
|
148
171
|
/**
|
|
149
172
|
* AT (TIMESTAMP|VERSION) clause for this scan, or `undefined` when the scan
|
|
@@ -184,6 +207,8 @@ export interface TableFunctionConfig<
|
|
|
184
207
|
argDocs?: Record<string, string>;
|
|
185
208
|
/** Argument defaults */
|
|
186
209
|
argDefaults?: Record<string, any>;
|
|
210
|
+
/** Authoritative typed defaults: one row, defaulted parameters only. */
|
|
211
|
+
parameterDefaultValues?: VgiBatch | null;
|
|
187
212
|
/**
|
|
188
213
|
* Per-argument discovery constraints (choices / ge / le / gt / lt / pattern),
|
|
189
214
|
* keyed by argument name. Surfaced via `vgi_function_arguments()` for agent
|
|
@@ -227,6 +252,45 @@ export interface TableFunctionConfig<
|
|
|
227
252
|
) => void | Promise<void>;
|
|
228
253
|
/** Cardinality hints */
|
|
229
254
|
cardinality?: (params: TableBindParams<TArgs>) => TableCardinality | Promise<TableCardinality>;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Divide this scan into named, independently redeemable splits.
|
|
258
|
+
*
|
|
259
|
+
* Declaring it (together with `meta.supportsSplits`) is what opts a function
|
|
260
|
+
* into the split path; omitting it means the whole scan is one unit of work.
|
|
261
|
+
*
|
|
262
|
+
* A split *names* work rather than describing it. "These three files at
|
|
263
|
+
* version 47" survives a retry; "rows 0-999 of whatever this returns now"
|
|
264
|
+
* does not — and a distributed engine WILL retry, so the difference is
|
|
265
|
+
* correctness, not tidiness. The same split may also be redeemed more than
|
|
266
|
+
* once (recursive CTEs, re-collected DataFrames, task retry) and may be
|
|
267
|
+
* abandoned mid-stream (LIMIT, TopK, an empty join build side); neither is an
|
|
268
|
+
* error.
|
|
269
|
+
*
|
|
270
|
+
* Set only `payload` on each split. The framework stamps the consistency
|
|
271
|
+
* anchor, the bind fingerprint and (where a key exists) the seal.
|
|
272
|
+
*
|
|
273
|
+
* Size splits into comparable units of work and honour `targetSplitBytes`: a
|
|
274
|
+
* claiming client treats them as interchangeable because it cannot see
|
|
275
|
+
* per-split cost, so wildly uneven splits leave its makespan bounded by the
|
|
276
|
+
* largest one.
|
|
277
|
+
*/
|
|
278
|
+
plan?: (
|
|
279
|
+
params: TableBindParams<TArgs>,
|
|
280
|
+
request: TableFunctionPlanRequest,
|
|
281
|
+
) => PlanResult | Promise<PlanResult>;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Called on a split init with the VERIFIED payloads for the splits this
|
|
285
|
+
* connection claimed — the envelope is already opened and stripped, so
|
|
286
|
+
* unverified bytes never reach here.
|
|
287
|
+
*
|
|
288
|
+
* Any state carried from planning to reading must live in cross-process
|
|
289
|
+
* storage keyed by `execution_id`: the process that plans is, in the general
|
|
290
|
+
* case, not the process that reads — and under a distributed engine it is not
|
|
291
|
+
* even the same host.
|
|
292
|
+
*/
|
|
293
|
+
onSplit?: (payloads: Uint8Array[], params: TableBindParams<TArgs>) => void | Promise<void>;
|
|
230
294
|
/**
|
|
231
295
|
* Per-column statistics for the function's output. Returned to DuckDB via
|
|
232
296
|
* the `table_function_statistics` RPC; the optimizer uses min/max to
|
|
@@ -256,7 +320,10 @@ export interface TableFunctionConfig<
|
|
|
256
320
|
/** Opt in to DuckDB's late-materialization SEMI-join rewrite; requires a
|
|
257
321
|
* UNIQUE, snapshot-stable rowid column. FunctionInfo.late_materialization. */
|
|
258
322
|
lateMaterialization?: boolean;
|
|
259
|
-
|
|
323
|
+
filterSemanticProfiles?: string[];
|
|
324
|
+
additionalFilterFunctions?: import("./types.js").FilterFunctionCapability[];
|
|
325
|
+
runtimeFilterAlgorithms?: import("./types.js").FilterFunctionCapability[];
|
|
326
|
+
filterEvaluationContexts?: import("./types.js").EvaluationContextCapability[];
|
|
260
327
|
autoApplyFilters?: boolean;
|
|
261
328
|
stability?: FunctionStability;
|
|
262
329
|
examples?: FunctionExample[];
|
|
@@ -275,12 +342,33 @@ export interface TableFunctionConfig<
|
|
|
275
342
|
supportsBatchIndex?: boolean;
|
|
276
343
|
/** Hive-style partition-columns mode; FunctionInfo.partition_kind. */
|
|
277
344
|
partitionKind?: "NOT_PARTITIONED" | "SINGLE_VALUE_PARTITIONS" | "OVERLAPPING_PARTITIONS" | "DISJOINT_PARTITIONS";
|
|
345
|
+
/**
|
|
346
|
+
* This function divides its scan into named, independently redeemable splits;
|
|
347
|
+
* FunctionInfo.supports_splits. Declare it together with `plan` and `onSplit`.
|
|
348
|
+
*
|
|
349
|
+
* It is what a distributed engine reads to decide it can retry a task against
|
|
350
|
+
* this function, and what makes the client call `plan()` at all — without it a
|
|
351
|
+
* split-capable function is scanned as one undivided unit, silently.
|
|
352
|
+
*/
|
|
353
|
+
supportsSplits?: boolean;
|
|
354
|
+
/**
|
|
355
|
+
* How long a minted split token stays redeemable, or undefined for UNBOUNDED
|
|
356
|
+
* (NOT "expires immediately"); FunctionInfo.split_token_ttl_seconds.
|
|
357
|
+
*
|
|
358
|
+
* A client refuses a plan whose TTL is below its own scheduling horizon,
|
|
359
|
+
* because an expired token is a failed query rather than a degradation:
|
|
360
|
+
* nothing re-plans when one expires, since a distributed engine retries the
|
|
361
|
+
* serialized task it was handed and has no path back to the planner.
|
|
362
|
+
*/
|
|
363
|
+
splitTokenTtlSeconds?: number;
|
|
278
364
|
}
|
|
279
365
|
|
|
280
366
|
export function defineTableFunction<
|
|
281
367
|
TArgs = Record<string, any>,
|
|
282
368
|
TState = null,
|
|
283
369
|
>(config: TableFunctionConfig<TArgs, TState>): VgiFunction {
|
|
370
|
+
assertArrowTypes((config as any).args, `defineTableFunction("${config.name}"): args`);
|
|
371
|
+
|
|
284
372
|
// Build argument specs
|
|
285
373
|
const specs: ArgumentSpec[] = [];
|
|
286
374
|
let posIdx = 0;
|
|
@@ -298,7 +386,10 @@ export function defineTableFunction<
|
|
|
298
386
|
isAnyType: isAny,
|
|
299
387
|
isVarargs: varargsSet.has(name),
|
|
300
388
|
doc: config.argDocs?.[name],
|
|
301
|
-
...constraintSpecFields(
|
|
389
|
+
...constraintSpecFields(
|
|
390
|
+
config.argConstraints?.[name],
|
|
391
|
+
config.argDefaults?.[name],
|
|
392
|
+
),
|
|
302
393
|
});
|
|
303
394
|
}
|
|
304
395
|
}
|
|
@@ -311,11 +402,15 @@ export function defineTableFunction<
|
|
|
311
402
|
filterPushdown: config.filterPushdown,
|
|
312
403
|
samplingPushdown: config.samplingPushdown,
|
|
313
404
|
lateMaterialization: config.lateMaterialization,
|
|
314
|
-
|
|
405
|
+
filterSemanticProfiles: config.filterSemanticProfiles,
|
|
406
|
+
additionalFilterFunctions: config.additionalFilterFunctions,
|
|
407
|
+
runtimeFilterAlgorithms: config.runtimeFilterAlgorithms,
|
|
408
|
+
filterEvaluationContexts: config.filterEvaluationContexts,
|
|
315
409
|
autoApplyFilters: config.autoApplyFilters,
|
|
316
410
|
examples: config.examples,
|
|
317
411
|
categories: config.categories,
|
|
318
412
|
tags: config.tags,
|
|
413
|
+
parameterDefaultValues: config.parameterDefaultValues,
|
|
319
414
|
maxWorkers: config.maxWorkers,
|
|
320
415
|
requiredSettings: config.requiredSettings,
|
|
321
416
|
requiredSecrets: config.requiredSecrets,
|
|
@@ -325,6 +420,8 @@ export function defineTableFunction<
|
|
|
325
420
|
distinctDependent: config.distinctDependent,
|
|
326
421
|
supportsBatchIndex: config.supportsBatchIndex,
|
|
327
422
|
partitionKind: config.partitionKind,
|
|
423
|
+
supportsSplits: config.supportsSplits,
|
|
424
|
+
splitTokenTtlSeconds: config.splitTokenTtlSeconds,
|
|
328
425
|
};
|
|
329
426
|
|
|
330
427
|
function extractArgs(request: BindRequest): TArgs {
|
|
@@ -342,8 +439,9 @@ export function defineTableFunction<
|
|
|
342
439
|
// converts positional args to named args)
|
|
343
440
|
val = request.arguments.get(spec.name, defaultVal);
|
|
344
441
|
}
|
|
345
|
-
// Arrow Int64
|
|
346
|
-
|
|
442
|
+
// Arrow Int64 arrives as bigint. Narrow when lossless, keep the
|
|
443
|
+
// bigint when not — see narrowArgValue.
|
|
444
|
+
val = narrowArgValue(val);
|
|
347
445
|
// Enforce declared constraints at bind (table args are all bind-time).
|
|
348
446
|
const constraints = config.argConstraints?.[spec.name];
|
|
349
447
|
if (constraints) validateConstConstraints(spec.name, constraints, val);
|
|
@@ -364,6 +462,7 @@ export function defineTableFunction<
|
|
|
364
462
|
const result = await config.onBind({
|
|
365
463
|
args, bindCall: request, settings, secrets,
|
|
366
464
|
resolvedSecretsProvided: request.resolved_secrets_provided ?? false,
|
|
465
|
+
argumentNames: request.argument_names ?? null,
|
|
367
466
|
});
|
|
368
467
|
return {
|
|
369
468
|
output_schema: result.outputSchema,
|
|
@@ -378,6 +477,28 @@ export function defineTableFunction<
|
|
|
378
477
|
const executionId = new Uint8Array(16);
|
|
379
478
|
crypto.getRandomValues(executionId);
|
|
380
479
|
|
|
480
|
+
// A SPLIT init is neither primary nor secondary. It carries an
|
|
481
|
+
// execution_id like a secondary, so it must not re-run global init — but
|
|
482
|
+
// unlike a secondary it MUST run user code, because the payload is what
|
|
483
|
+
// names the work. The secondary branch below runs none, which is why this
|
|
484
|
+
// is a third branch rather than a flag on that one.
|
|
485
|
+
if (request.split_payloads) {
|
|
486
|
+
if (config.onSplit) {
|
|
487
|
+
await config.onSplit(request.split_payloads, {
|
|
488
|
+
args: extractArgs(request.bind_call),
|
|
489
|
+
bindCall: request.bind_call,
|
|
490
|
+
settings: batchToScalarDict(request.bind_call.settings),
|
|
491
|
+
secrets: batchToSecretDict(request.bind_call.secrets),
|
|
492
|
+
resolvedSecretsProvided: request.bind_call.resolved_secrets_provided ?? false,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
max_workers: config.maxWorkers ?? 1,
|
|
497
|
+
execution_id: request.execution_id ?? executionId,
|
|
498
|
+
opaque_data: null,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
381
502
|
if (request.execution_id) {
|
|
382
503
|
// Secondary init - reuse execution ID
|
|
383
504
|
return {
|
|
@@ -425,9 +546,15 @@ export function defineTableFunction<
|
|
|
425
546
|
// Deserialize pushdown filters. Pass a join-keys column lookup so that
|
|
426
547
|
// filters DuckDB promoted to join_keys (IN/OR lists, etc.) are
|
|
427
548
|
// materialized as InFilters rather than silently dropped.
|
|
428
|
-
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
|
+
};
|
|
429
556
|
const pushdownFilters = request.pushdown_filters
|
|
430
|
-
? deserializeFilters(request.pushdown_filters,
|
|
557
|
+
? deserializeFilters(request.pushdown_filters, filterOptions)
|
|
431
558
|
: undefined;
|
|
432
559
|
|
|
433
560
|
const boundStorage = new BoundStorage(globalStorage, response.execution_id);
|
|
@@ -440,11 +567,26 @@ export function defineTableFunction<
|
|
|
440
567
|
settings,
|
|
441
568
|
secrets,
|
|
442
569
|
pushdownFilters,
|
|
570
|
+
splitPayloads: request.split_payloads ?? undefined,
|
|
443
571
|
storage: boundStorage,
|
|
444
572
|
atUnit: request.bind_call.at_unit ?? undefined,
|
|
445
573
|
atValue: request.bind_call.at_value ?? undefined,
|
|
446
574
|
};
|
|
447
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
|
+
|
|
448
590
|
const state = config.initialState
|
|
449
591
|
? config.initialState(processParams)
|
|
450
592
|
: (null as TState);
|
|
@@ -469,23 +611,20 @@ export function defineTableFunction<
|
|
|
469
611
|
if (ifModifiedSince !== undefined) pState.processParams.ifModifiedSince = ifModifiedSince;
|
|
470
612
|
|
|
471
613
|
// Dynamic filter pushdown: DuckDB's Top-N optimizer tightens filters
|
|
472
|
-
// between ticks and serializes
|
|
614
|
+
// between ticks and serializes an atomic v2 delta into the tick
|
|
473
615
|
// batch's custom metadata under `vgi_pushdown_filters` (base64 of a
|
|
474
|
-
// filter IPC stream).
|
|
475
|
-
//
|
|
616
|
+
// filter IPC stream). Apply it to the prior snapshot so process()
|
|
617
|
+
// sees the revised advisory predicate state.
|
|
476
618
|
const encoded = tickMetadata.get("vgi_pushdown_filters");
|
|
477
619
|
if (!encoded) return;
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
// Malformed dynamic-filter update: keep the previous filter. Not
|
|
487
|
-
// fatal — this is a best-effort optimization hint from DuckDB.
|
|
488
|
-
}
|
|
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);
|
|
489
628
|
},
|
|
490
629
|
producerFn: async (
|
|
491
630
|
pState: { state: TState; processParams: TableProcessParams<TArgs> },
|
|
@@ -523,6 +662,32 @@ export function defineTableFunction<
|
|
|
523
662
|
}
|
|
524
663
|
: undefined,
|
|
525
664
|
|
|
665
|
+
// Wired through with decoded arguments, exactly as cardinality and
|
|
666
|
+
// statistics are.
|
|
667
|
+
//
|
|
668
|
+
// It was previously declared on the config type and never passed to the
|
|
669
|
+
// returned function, so the hook was DEAD: a worker could declare
|
|
670
|
+
// `supportsSplits` and a `plan`, advertise split capability on the wire, and
|
|
671
|
+
// silently serve the framework default of one empty-payload split. Nothing
|
|
672
|
+
// failed — the scan just quietly stopped being divided.
|
|
673
|
+
plan: config.plan
|
|
674
|
+
? (request: TableFunctionPlanRequest) => {
|
|
675
|
+
const args = extractArgs(request.bind_call);
|
|
676
|
+
const settings = batchToScalarDict(request.bind_call.settings);
|
|
677
|
+
const secrets = batchToSecretDict(request.bind_call.secrets);
|
|
678
|
+
return config.plan!(
|
|
679
|
+
{
|
|
680
|
+
args,
|
|
681
|
+
bindCall: request.bind_call,
|
|
682
|
+
settings,
|
|
683
|
+
secrets,
|
|
684
|
+
resolvedSecretsProvided: request.bind_call.resolved_secrets_provided ?? false,
|
|
685
|
+
},
|
|
686
|
+
request,
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
: undefined,
|
|
690
|
+
|
|
526
691
|
statistics: config.statistics
|
|
527
692
|
? (request: TableFunctionCardinalityRequest) => {
|
|
528
693
|
const args = extractArgs(request.bind_call);
|
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;
|
|
@@ -102,6 +125,38 @@ export interface FunctionMeta {
|
|
|
102
125
|
* `supports_batch_index`.
|
|
103
126
|
*/
|
|
104
127
|
supportsBatchIndex?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* table (generator): the scan divides into named, independently redeemable
|
|
130
|
+
* splits (see the `plan` / `onSplit` hooks). Surfaces as FunctionInfo
|
|
131
|
+
* `supports_splits`.
|
|
132
|
+
*
|
|
133
|
+
* The declaration is what a distributed engine reads to decide whether it can
|
|
134
|
+
* retry a task: a split NAMES its work, so re-running one reads exactly the
|
|
135
|
+
* same rows. Without it, the division lives behind an opaque worker-side queue
|
|
136
|
+
* and a retry reads neither what it read before nor what it missed.
|
|
137
|
+
*/
|
|
138
|
+
supportsSplits?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* table (generator): the worker applies every pushed filter EXACTLY, so the
|
|
141
|
+
* engine may drop its own copy rather than re-filtering. Wrong answers if
|
|
142
|
+
* declared falsely — leave unset unless certain.
|
|
143
|
+
*/
|
|
144
|
+
filtersExactlyApplied?: boolean;
|
|
145
|
+
/**
|
|
146
|
+
* table (generator): addressable positions in the data, for incremental and
|
|
147
|
+
* streaming reads. The engine owns the checkpoint; this declares only that
|
|
148
|
+
* positions are meaningful to externalize.
|
|
149
|
+
*/
|
|
150
|
+
supportsPositions?: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* table (generator): how long a split token stays redeemable.
|
|
153
|
+
*
|
|
154
|
+
* UNSET MEANS UNBOUNDED, not "expires immediately" — a client must not assume
|
|
155
|
+
* a TTL exists, or long-running streams are foreclosed. A client rejects a
|
|
156
|
+
* plan whose TTL is below its own planning-to-task-start horizon, which on a
|
|
157
|
+
* busy cluster is hours rather than seconds.
|
|
158
|
+
*/
|
|
159
|
+
splitTokenTtlSeconds?: number;
|
|
105
160
|
/**
|
|
106
161
|
* table (generator): Hive-style partition-columns mode. Functions declare
|
|
107
162
|
* a PartitionKind and annotate bind-schema fields; emitted batches carry
|
|
@@ -180,6 +235,14 @@ export interface VgiFunction {
|
|
|
180
235
|
accumulatedState?: any,
|
|
181
236
|
): StreamHandlers;
|
|
182
237
|
cardinality?(request: TableFunctionCardinalityRequest): TableCardinality | Promise<TableCardinality>;
|
|
238
|
+
/**
|
|
239
|
+
* Divide a scan into named, independently redeemable splits. Absent means the
|
|
240
|
+
* function is not split-capable: the whole scan is one unit of work, and the
|
|
241
|
+
* framework answers with a single empty-payload split.
|
|
242
|
+
*/
|
|
243
|
+
plan?(request: import("../protocol/serializers/splits.js").TableFunctionPlanRequest):
|
|
244
|
+
| import("../protocol/serializers/splits.js").PlanResult
|
|
245
|
+
| Promise<import("../protocol/serializers/splits.js").PlanResult>;
|
|
183
246
|
/**
|
|
184
247
|
* Per-column statistics for this table function's output given the user's
|
|
185
248
|
* bind-time arguments. Returns null/[] when stats are unknown. Wired to the
|