@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.
Files changed (121) hide show
  1. package/README.md +52 -8
  2. package/dist/arguments/argument-spec.d.ts +57 -2
  3. package/dist/arrow/impl-arrowjs/index.d.ts +1 -1
  4. package/dist/arrow/impl-arrowjs/iterate.d.ts +10 -1
  5. package/dist/arrow/impl-arrowjs/schema.d.ts +2 -0
  6. package/dist/arrow/impl-flechette/index.d.ts +1 -1
  7. package/dist/arrow/impl-flechette/iterate.d.ts +10 -1
  8. package/dist/arrow/impl-flechette/schema.d.ts +2 -0
  9. package/dist/arrow/index.d.ts +1 -1
  10. package/dist/arrow/schema-types.d.ts +3 -1
  11. package/dist/arrow/types.d.ts +39 -3
  12. package/dist/catalog/composite.d.ts +13 -13
  13. package/dist/catalog/descriptors.d.ts +7 -3
  14. package/dist/catalog/interface.d.ts +108 -38
  15. package/dist/catalog/read-only.d.ts +32 -15
  16. package/dist/client/client.d.ts +37 -30
  17. package/dist/client/types.d.ts +2 -0
  18. package/dist/client-entry.d.ts +1 -1
  19. package/dist/client-entry.js +791 -447
  20. package/dist/client-entry.js.map +18 -17
  21. package/dist/filter-pushdown/capabilities.d.ts +3 -0
  22. package/dist/filter-pushdown/deserialize.d.ts +14 -15
  23. package/dist/filter-pushdown/evaluate.d.ts +14 -44
  24. package/dist/filter-pushdown/index.d.ts +2 -2
  25. package/dist/filter-pushdown/types.d.ts +120 -68
  26. package/dist/functions/aggregate.d.ts +4 -0
  27. package/dist/functions/registry.d.ts +5 -5
  28. package/dist/functions/scalar.d.ts +6 -1
  29. package/dist/functions/table-buffering.d.ts +9 -0
  30. package/dist/functions/table-in-out.d.ts +13 -0
  31. package/dist/functions/table.d.ts +72 -2
  32. package/dist/functions/types.d.ts +60 -2
  33. package/dist/generated/vgi-client.d.ts +114 -26
  34. package/dist/generated/vgi-protocol-schemas.d.ts +34 -0
  35. package/dist/http/fetch.d.ts +16 -0
  36. package/dist/index.core.d.ts +4 -3
  37. package/dist/index.js +3396 -2064
  38. package/dist/index.js.map +55 -49
  39. package/dist/metadata/types.d.ts +15 -2
  40. package/dist/protocol/handlers/catalog/shared.d.ts +0 -8
  41. package/dist/protocol/handlers/function.d.ts +11 -0
  42. package/dist/protocol/handlers/shared.d.ts +3 -1
  43. package/dist/protocol/serializers/bind.d.ts +1 -2
  44. package/dist/protocol/serializers/cardinality.d.ts +1 -2
  45. package/dist/protocol/serializers/splits.d.ts +126 -0
  46. package/dist/protocol/types.d.ts +36 -3
  47. package/dist/request-auth.d.ts +28 -0
  48. package/dist/schema-path.d.ts +7 -0
  49. package/dist/serve-entry.d.ts +25 -0
  50. package/dist/serve-entry.js +1129 -569
  51. package/dist/serve-entry.js.map +36 -32
  52. package/dist/split-token.d.ts +88 -0
  53. package/dist/types.d.ts +8 -0
  54. package/dist/worker-cf-entry.js +2928 -1689
  55. package/dist/worker-cf-entry.js.map +51 -44
  56. package/dist/worker.d.ts +1 -1
  57. package/package.json +3 -3
  58. package/src/arguments/argument-spec.ts +93 -4
  59. package/src/arguments/arguments.ts +0 -1
  60. package/src/arrow/impl-arrowjs/index.ts +1 -1
  61. package/src/arrow/impl-arrowjs/iterate.ts +21 -0
  62. package/src/arrow/impl-arrowjs/schema.ts +4 -0
  63. package/src/arrow/impl-flechette/index.ts +1 -1
  64. package/src/arrow/impl-flechette/iterate.ts +24 -1
  65. package/src/arrow/impl-flechette/schema.ts +4 -0
  66. package/src/arrow/index.ts +1 -1
  67. package/src/arrow/schema-types.ts +4 -0
  68. package/src/arrow/types.ts +39 -3
  69. package/src/catalog/composite.ts +26 -26
  70. package/src/catalog/descriptors.ts +12 -3
  71. package/src/catalog/interface.ts +217 -72
  72. package/src/catalog/read-only.ts +141 -84
  73. package/src/client/client.ts +97 -73
  74. package/src/client/types.ts +2 -0
  75. package/src/client-entry.ts +2 -1
  76. package/src/filter-pushdown/capabilities.ts +13 -0
  77. package/src/filter-pushdown/collector.ts +22 -35
  78. package/src/filter-pushdown/deserialize.ts +657 -221
  79. package/src/filter-pushdown/evaluate.ts +410 -499
  80. package/src/filter-pushdown/index.ts +16 -2
  81. package/src/filter-pushdown/types.ts +88 -71
  82. package/src/functions/aggregate.ts +12 -1
  83. package/src/functions/copy-from.ts +2 -1
  84. package/src/functions/copy-to.ts +2 -1
  85. package/src/functions/registry.ts +24 -22
  86. package/src/functions/scalar.ts +37 -0
  87. package/src/functions/table-buffering.ts +48 -6
  88. package/src/functions/table-in-out.ts +194 -33
  89. package/src/functions/table.ts +187 -22
  90. package/src/functions/types.ts +64 -1
  91. package/src/generated/vgi-client.ts +122 -28
  92. package/src/generated/vgi-protocol-schemas.ts +455 -58
  93. package/src/http/fetch.ts +129 -2
  94. package/src/index.core.ts +4 -2
  95. package/src/metadata/resolve.ts +43 -1
  96. package/src/metadata/serialize.ts +43 -5
  97. package/src/metadata/types.ts +17 -1
  98. package/src/protocol/dispatch.ts +1 -1
  99. package/src/protocol/handlers/aggregate.ts +23 -12
  100. package/src/protocol/handlers/catalog/admin.ts +47 -54
  101. package/src/protocol/handlers/catalog/index_methods.ts +6 -13
  102. package/src/protocol/handlers/catalog/macro.ts +12 -20
  103. package/src/protocol/handlers/catalog/shared.ts +12 -57
  104. package/src/protocol/handlers/catalog/table.ts +49 -112
  105. package/src/protocol/handlers/catalog/view.ts +18 -22
  106. package/src/protocol/handlers/function.ts +311 -18
  107. package/src/protocol/handlers/shared.ts +12 -3
  108. package/src/protocol/handlers/table-buffering.ts +11 -8
  109. package/src/protocol/serializers/bind.ts +69 -67
  110. package/src/protocol/serializers/cardinality.ts +7 -7
  111. package/src/protocol/serializers/init.ts +52 -10
  112. package/src/protocol/serializers/splits.ts +221 -0
  113. package/src/protocol/state-serializer.ts +55 -1
  114. package/src/protocol/types.ts +36 -3
  115. package/src/request-auth.ts +73 -0
  116. package/src/schema-path.ts +25 -0
  117. package/src/serve-entry.ts +69 -2
  118. package/src/split-token.ts +297 -0
  119. package/src/types.ts +9 -0
  120. package/src/worker-cf-entry.ts +2 -1
  121. package/src/worker.ts +79 -14
@@ -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
- supportedExpressionFilters?: string[];
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(config.argConstraints?.[name]),
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
- supportedExpressionFilters: config.supportedExpressionFilters,
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 values come through as BigInt — coerce to number
346
- if (typeof val === "bigint") val = safeNumber(val);
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 joinKeysLookup = buildJoinKeysLookup(request.join_keys);
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, joinKeysLookup)
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 the current filter into the tick
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). Decode and overwrite the current pushdown
475
- // filters so process() sees the updated value.
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
- try {
479
- const bytes = base64Decode(encoded);
480
- const filterBatch = deserializeFilterBatch(bytes);
481
- if (filterBatch) {
482
- const updated = deserializeFilters(filterBatch, joinKeysLookup);
483
- pState.processParams.pushdownFilters = updated;
484
- }
485
- } catch {
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);
@@ -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
- supportedExpressionFilters?: string[];
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