@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.
Files changed (90) hide show
  1. package/README.md +26 -1
  2. package/dist/arguments/argument-spec.d.ts +4 -2
  3. package/dist/catalog/composite.d.ts +13 -13
  4. package/dist/catalog/descriptors.d.ts +7 -3
  5. package/dist/catalog/interface.d.ts +67 -40
  6. package/dist/catalog/read-only.d.ts +32 -15
  7. package/dist/client/client.d.ts +36 -29
  8. package/dist/client/types.d.ts +2 -0
  9. package/dist/client-entry.d.ts +1 -1
  10. package/dist/client-entry.js +441 -279
  11. package/dist/client-entry.js.map +12 -11
  12. package/dist/filter-pushdown/capabilities.d.ts +3 -0
  13. package/dist/filter-pushdown/deserialize.d.ts +14 -15
  14. package/dist/filter-pushdown/evaluate.d.ts +14 -44
  15. package/dist/filter-pushdown/index.d.ts +2 -2
  16. package/dist/filter-pushdown/types.d.ts +120 -68
  17. package/dist/functions/aggregate.d.ts +4 -0
  18. package/dist/functions/registry.d.ts +5 -5
  19. package/dist/functions/scalar.d.ts +6 -1
  20. package/dist/functions/table-buffering.d.ts +9 -0
  21. package/dist/functions/table-in-out.d.ts +13 -0
  22. package/dist/functions/table.d.ts +9 -2
  23. package/dist/functions/types.d.ts +22 -2
  24. package/dist/generated/vgi-client.d.ts +56 -27
  25. package/dist/generated/vgi-protocol-schemas.d.ts +15 -0
  26. package/dist/http/fetch.d.ts +7 -1
  27. package/dist/index.core.d.ts +2 -1
  28. package/dist/index.js +2317 -1354
  29. package/dist/index.js.map +43 -40
  30. package/dist/metadata/types.d.ts +15 -2
  31. package/dist/protocol/handlers/shared.d.ts +3 -1
  32. package/dist/protocol/serializers/splits.d.ts +1 -1
  33. package/dist/protocol/types.d.ts +9 -3
  34. package/dist/schema-path.d.ts +7 -0
  35. package/dist/serve-entry.d.ts +22 -1
  36. package/dist/serve-entry.js +412 -238
  37. package/dist/serve-entry.js.map +27 -26
  38. package/dist/split-token.d.ts +1 -1
  39. package/dist/types.d.ts +8 -0
  40. package/dist/worker-cf-entry.js +2030 -1201
  41. package/dist/worker-cf-entry.js.map +40 -36
  42. package/dist/worker.d.ts +1 -1
  43. package/package.json +3 -3
  44. package/src/arguments/argument-spec.ts +12 -4
  45. package/src/catalog/composite.ts +26 -26
  46. package/src/catalog/descriptors.ts +12 -3
  47. package/src/catalog/interface.ts +98 -48
  48. package/src/catalog/read-only.ts +124 -67
  49. package/src/client/client.ts +92 -72
  50. package/src/client/types.ts +2 -0
  51. package/src/client-entry.ts +2 -1
  52. package/src/filter-pushdown/capabilities.ts +13 -0
  53. package/src/filter-pushdown/collector.ts +22 -35
  54. package/src/filter-pushdown/deserialize.ts +657 -221
  55. package/src/filter-pushdown/evaluate.ts +410 -499
  56. package/src/filter-pushdown/index.ts +16 -2
  57. package/src/filter-pushdown/types.ts +88 -71
  58. package/src/functions/aggregate.ts +9 -1
  59. package/src/functions/registry.ts +24 -22
  60. package/src/functions/scalar.ts +26 -0
  61. package/src/functions/table-buffering.ts +46 -7
  62. package/src/functions/table-in-out.ts +187 -30
  63. package/src/functions/table.ts +58 -20
  64. package/src/functions/types.ts +24 -1
  65. package/src/generated/vgi-client.ts +61 -29
  66. package/src/generated/vgi-protocol-schemas.ts +225 -54
  67. package/src/http/fetch.ts +12 -0
  68. package/src/index.core.ts +2 -0
  69. package/src/metadata/resolve.ts +43 -1
  70. package/src/metadata/serialize.ts +43 -5
  71. package/src/metadata/types.ts +17 -1
  72. package/src/protocol/dispatch.ts +1 -1
  73. package/src/protocol/handlers/aggregate.ts +13 -7
  74. package/src/protocol/handlers/catalog/admin.ts +6 -6
  75. package/src/protocol/handlers/catalog/index_methods.ts +2 -2
  76. package/src/protocol/handlers/catalog/macro.ts +4 -4
  77. package/src/protocol/handlers/catalog/shared.ts +2 -0
  78. package/src/protocol/handlers/catalog/table.ts +16 -16
  79. package/src/protocol/handlers/catalog/view.ts +5 -5
  80. package/src/protocol/handlers/function.ts +2 -2
  81. package/src/protocol/handlers/shared.ts +12 -3
  82. package/src/protocol/handlers/table-buffering.ts +4 -4
  83. package/src/protocol/serializers/bind.ts +15 -6
  84. package/src/protocol/serializers/splits.ts +4 -2
  85. package/src/protocol/types.ts +9 -3
  86. package/src/schema-path.ts +25 -0
  87. package/src/serve-entry.ts +65 -3
  88. package/src/split-token.ts +2 -2
  89. package/src/types.ts +9 -0
  90. 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 { assertArrowType, assertArrowTypes, narrowArgValue } from "../arguments/argument-spec.js";
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, joinKeysLookup)
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
- // The actual collect+finalize work is deferred into the first
311
- // producerFn call so the (now-async) FunctionStorage can be awaited
312
- // there. Storing the materialized batches on the per-call state.
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: { batchIdx: 0 }, batches: null as VgiBatch[] | null }),
378
+ producerInit: () => ({ state: { cursor: -1, materialized: false } }),
316
379
  producerFn: async (
317
- pState: { state: { batchIdx: number }; batches: VgiBatch[] | null },
380
+ pState: { state: { cursor: number; materialized: boolean } },
318
381
  out: OutputCollector
319
382
  ) => {
320
- if (pState.batches == null) {
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
- pState.batches = config.finalize
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
- if (pState.state.batchIdx >= pState.batches.length) {
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
- out.emit(pState.batches[pState.state.batchIdx]);
348
- pState.state.batchIdx++;
349
- if (pState.state.batchIdx >= pState.batches.length) {
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 ?? undefined,
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
- if (config.autoApplyFilters && pushdownFilters) {
398
- wrappedOut = new FilteringOutputCollector(wrappedOut, pushdownFilters) as unknown as OutputCollector;
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, joinKeysLookup)
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 ?? undefined,
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
- if (config.autoApplyFilters && pushdownFilters) {
846
- wrappedOut = new FilteringOutputCollector(wrappedOut, pushdownFilters) as unknown as OutputCollector;
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
  },
@@ -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
- supportedExpressionFilters?: string[];
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(config.argConstraints?.[name]),
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
- supportedExpressionFilters: config.supportedExpressionFilters,
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 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
+ };
529
556
  const pushdownFilters = request.pushdown_filters
530
- ? deserializeFilters(request.pushdown_filters, joinKeysLookup)
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 the current filter into the tick
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). Decode and overwrite the current pushdown
576
- // 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.
577
618
  const encoded = tickMetadata.get("vgi_pushdown_filters");
578
619
  if (!encoded) return;
579
- try {
580
- const bytes = base64Decode(encoded);
581
- const filterBatch = deserializeFilterBatch(bytes);
582
- if (filterBatch) {
583
- const updated = deserializeFilters(filterBatch, joinKeysLookup);
584
- pState.processParams.pushdownFilters = updated;
585
- }
586
- } catch {
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> },
@@ -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;