@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
@@ -3,7 +3,7 @@
3
3
  // Two-phase: INPUT phase receives and transforms batches,
4
4
  // FINALIZE phase emits final results.
5
5
 
6
- import { type VgiSchema, schema, type VgiField, type VgiDataType, type VgiBatch, nullType, withBatchMetadata } from "../arrow/index.js";
6
+ import { type VgiSchema, schema, type VgiField, type VgiDataType, type VgiBatch, nullType, withBatchMetadata, serializeBatch, deserializeBatch } from "../arrow/index.js";
7
7
  import type { OutputCollector } from "@query-farm/vgi-rpc";
8
8
  import { DEFAULT_MAX_WORKERS, TableInOutPhase } from "../types.js";
9
9
  import type {
@@ -18,19 +18,33 @@ import type {
18
18
  StreamHandlers,
19
19
  FunctionExample,
20
20
  } from "./types.js";
21
- import type { ArgumentSpec } from "../arguments/argument-spec.js";
22
- import { batchToScalarDict, batchToSecretDict, projectSchema, projectBatch, emptyBatch } from "../util/arrow/index.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";
29
+ import { batchToScalarDict, batchToSecretDict, projectSchema, projectBatch, emptyBatch, safeNumber } from "../util/arrow/index.js";
23
30
  import { CACHE_IF_MODIFIED_SINCE_KEY, CACHE_IF_NONE_MATCH_KEY } from "../cache-control.js";
24
31
  import {
25
- buildJoinKeysLookup,
26
32
  deserializeFilters,
27
33
  FilteringOutputCollector,
28
34
  type PushdownFilters,
29
35
  } from "../filter-pushdown/index.js";
30
36
  import { FunctionStability } from "../types.js";
31
- import { BoundStorage, storage as defaultStorage } from "./storage.js";
37
+ import { BoundStorage, storage as defaultStorage, FrameworkNS } from "./storage.js";
32
38
  import { serializeUserState, deserializeUserState } from "../protocol/state-serializer.js";
33
39
 
40
+ /**
41
+ * State-log key the streaming FINALIZE drain uses within
42
+ * {@link FrameworkNS.STREAMING_FINALIZE}. There is exactly one flush per
43
+ * execution, so a single empty key is enough — matches vgi-python's
44
+ * `_STREAMING_FINALIZE_KEY = b""`.
45
+ */
46
+ const STREAMING_FINALIZE_KEY = new Uint8Array(0);
47
+
34
48
  // ============================================================================
35
49
  // Table In-Out parameter bundles (reuse table function's)
36
50
  // ============================================================================
@@ -40,6 +54,8 @@ export interface TableInOutBindParams<TArgs = Record<string, any>> {
40
54
  bindCall: BindRequest;
41
55
  settings: Record<string, any>;
42
56
  secrets: Record<string, Record<string, any>>;
57
+ /** Resolved names aligned with the complete logical argument order. */
58
+ argumentNames?: (string | null)[] | null;
43
59
  }
44
60
 
45
61
  export interface TableInOutProcessParams<TArgs = Record<string, any>> {
@@ -91,6 +107,12 @@ export interface TableInOutConfig<
91
107
  namedArgs?: Record<string, VgiDataType>;
92
108
  /** Argument defaults */
93
109
  argDefaults?: Record<string, any>;
110
+ /** Authoritative typed defaults: one row, defaulted parameters only. */
111
+ parameterDefaultValues?: VgiBatch | null;
112
+ /** Argument descriptions keyed by argument name. */
113
+ argDocs?: Record<string, string>;
114
+ /** Discovery constraints, also enforced for scalar arguments at bind. */
115
+ argConstraints?: Record<string, ArgumentConstraints>;
94
116
  /** Bind: default passes through input schema. May be async. */
95
117
  onBind?: (params: TableInOutBindParams<TArgs>) =>
96
118
  | { outputSchema: VgiSchema; opaqueData?: Uint8Array }
@@ -132,6 +154,8 @@ export function defineTableInOutFunction<
132
154
  TArgs = Record<string, any>,
133
155
  TState = null,
134
156
  >(config: TableInOutConfig<TArgs, TState>): VgiFunction {
157
+ assertArrowTypes((config as any).args, `defineTableInOutFunction("${config.name}"): args`);
158
+
135
159
 
136
160
  // Build argument specs
137
161
  const specs: ArgumentSpec[] = [];
@@ -144,6 +168,11 @@ export function defineTableInOutFunction<
144
168
  name,
145
169
  position: posIdx++,
146
170
  arrowType: type,
171
+ doc: config.argDocs?.[name],
172
+ ...constraintSpecFields(
173
+ config.argConstraints?.[name],
174
+ config.argDefaults?.[name],
175
+ ),
147
176
  });
148
177
  }
149
178
  }
@@ -162,6 +191,11 @@ export function defineTableInOutFunction<
162
191
  name,
163
192
  position: name, // string position = named arg
164
193
  arrowType: type,
194
+ doc: config.argDocs?.[name],
195
+ ...constraintSpecFields(
196
+ config.argConstraints?.[name],
197
+ config.argDefaults?.[name],
198
+ ),
165
199
  });
166
200
  }
167
201
  }
@@ -176,6 +210,7 @@ export function defineTableInOutFunction<
176
210
  examples: config.examples,
177
211
  categories: config.categories,
178
212
  tags: config.tags,
213
+ parameterDefaultValues: config.parameterDefaultValues,
179
214
  maxWorkers: config.maxWorkers,
180
215
  requiredSettings: config.requiredSettings,
181
216
  requiredSecrets: config.requiredSecrets,
@@ -192,8 +227,11 @@ export function defineTableInOutFunction<
192
227
  : undefined;
193
228
  // For named args, position is a string (the arg name)
194
229
  let val = request.arguments.get(spec.position, defaultVal);
195
- // Arrow Int64 values come through as BigInt — coerce to number
196
- if (typeof val === "bigint") val = Number(val);
230
+ // Arrow Int64 arrives as bigint. Narrow when lossless, keep the
231
+ // bigint when not — see narrowArgValue.
232
+ val = narrowArgValue(val);
233
+ const constraints = config.argConstraints?.[spec.name];
234
+ if (constraints) validateConstConstraints(spec.name, constraints, val);
197
235
  args[spec.name] = val;
198
236
  }
199
237
  return args as TArgs;
@@ -214,6 +252,7 @@ export function defineTableInOutFunction<
214
252
  bindCall: request,
215
253
  settings,
216
254
  secrets,
255
+ argumentNames: request.argument_names ?? null,
217
256
  });
218
257
  return {
219
258
  output_schema: result.outputSchema,
@@ -276,9 +315,14 @@ export function defineTableInOutFunction<
276
315
  // Deserialize pushdown filters. Pass a join-keys column lookup so that
277
316
  // filters DuckDB promoted to join_keys (IN/OR lists, etc.) are
278
317
  // materialized as InFilters rather than silently dropped.
279
- const joinKeysLookup = buildJoinKeysLookup(request.join_keys);
280
318
  const pushdownFilters = request.pushdown_filters
281
- ? deserializeFilters(request.pushdown_filters, 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
+ })
282
326
  : undefined;
283
327
 
284
328
  // Create BoundStorage for cross-phase/cross-worker data sharing
@@ -302,18 +346,41 @@ export function defineTableInOutFunction<
302
346
  const phase = request.phase;
303
347
 
304
348
  if (phase === TableInOutPhase.FINALIZE) {
305
- // FINALIZE phase: producer mode.
306
- // The actual collect+finalize work is deferred into the first
307
- // producerFn call so the (now-async) FunctionStorage can be awaited
308
- // 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;
309
376
  return {
310
377
  outputSchema,
311
- producerInit: () => ({ state: { batchIdx: 0 }, batches: null as VgiBatch[] | null }),
378
+ producerInit: () => ({ state: { cursor: -1, materialized: false } }),
312
379
  producerFn: async (
313
- pState: { state: { batchIdx: number }; batches: VgiBatch[] | null },
380
+ pState: { state: { cursor: number; materialized: boolean } },
314
381
  out: OutputCollector
315
382
  ) => {
316
- if (pState.batches == null) {
383
+ if (!pState.state.materialized) {
317
384
  const finalizeStates: TState[] = [];
318
385
  if (accumulatedState != null) {
319
386
  finalizeStates.push(accumulatedState as TState);
@@ -332,24 +399,55 @@ export function defineTableInOutFunction<
332
399
  : (null as TState),
333
400
  );
334
401
  }
335
- pState.batches = config.finalize
402
+ const batches: VgiBatch[] = config.finalize
336
403
  ? await config.finalize(processParams, finalizeStates)
337
404
  : [];
405
+ for (const batch of batches) {
406
+ await boundStorage.stateAppend(
407
+ FrameworkNS.STREAMING_FINALIZE,
408
+ finalizeLogKey,
409
+ serializeBatch(batch),
410
+ );
411
+ }
412
+ // Set before the first emit so the flag is part of the userState
413
+ // this turn serializes — a continuation must never re-run
414
+ // finalize(), even when the log came back empty.
415
+ pState.state.materialized = true;
338
416
  }
339
- 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) {
340
424
  out.finish();
341
425
  return;
342
426
  }
343
- out.emit(pState.batches[pState.state.batchIdx]);
344
- pState.state.batchIdx++;
345
- if (pState.state.batchIdx >= pState.batches.length) {
346
- out.finish();
347
- }
427
+ const [logId, bytes] = rows[0];
428
+ out.emit(deserializeBatch(bytes) as unknown as VgiBatch);
429
+ pState.state.cursor = logId;
348
430
  },
349
431
  };
350
432
  }
351
433
 
352
- // INPUT phase: exchange mode
434
+ // INPUT phase: exchange mode. Any phase other than FINALIZE falls
435
+ // through to here, including a missing/undefined phase -- i.e. a
436
+ // caller that drove this function via table_function() instead of
437
+ // table_in_out_function(input=...). Reject that immediately rather
438
+ // than silently degrading to an empty inputSchema below, which
439
+ // disables the transport layer's own schema-conformance check and
440
+ // leaves both sides waiting on each other forever (confirmed live: a
441
+ // non-terminating exchange/continuation loop against a real deployed
442
+ // worker, not an error).
443
+ if (request.bind_call.input_schema == null) {
444
+ throw new Error(
445
+ `${config.name}: is a table-in-out function (it requires an input row ` +
446
+ `stream) but no input schema was supplied -- call it via ` +
447
+ `table_in_out_function(input=...), not table_function().`,
448
+ );
449
+ }
450
+
353
451
  const state = config.initialState
354
452
  ? config.initialState(processParams)
355
453
  : (null as TState);
@@ -368,7 +466,7 @@ export function defineTableInOutFunction<
368
466
 
369
467
  return {
370
468
  outputSchema,
371
- inputSchema: request.bind_call.input_schema ?? undefined,
469
+ inputSchema: request.bind_call.input_schema,
372
470
  exchangeInit: () => ({ state, processParams }),
373
471
  exchangeFn: async (
374
472
  eState: {
@@ -382,6 +480,7 @@ export function defineTableInOutFunction<
382
480
  // custom metadata (attached by the C++ WriteInputBatch), so process()
383
481
  // can answer a 0-row `notModified` batch instead of recomputing.
384
482
  applyRevalidationValidators(eState.processParams, input);
483
+ applyDynamicFilterUpdate(eState.processParams, input);
385
484
  // Bake emit metadata onto the batch (0-row HTTP survival), then
386
485
  // reconcile emitted batches to the (possibly projected) output schema
387
486
  // by name — a process() may emit its full declared schema and let the
@@ -390,8 +489,9 @@ export function defineTableInOutFunction<
390
489
  if (projIds) {
391
490
  wrappedOut = makeSchemaReconcilingCollector(wrappedOut, outputSchema);
392
491
  }
393
- if (config.autoApplyFilters && pushdownFilters) {
394
- 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;
395
495
  }
396
496
  await processFn(eState.processParams, eState.state, input, wrappedOut);
397
497
 
@@ -429,6 +529,29 @@ function applyRevalidationValidators(
429
529
  params.ifModifiedSince = md?.get(CACHE_IF_MODIFIED_SINCE_KEY);
430
530
  }
431
531
 
532
+ function applyDynamicFilterUpdate(
533
+ params: { pushdownFilters?: PushdownFilters },
534
+ input: VgiBatch,
535
+ ): void {
536
+ const encoded = ((input as any)?.metadata as Map<string, string> | undefined)?.get("vgi_pushdown_filters");
537
+ if (!encoded) return;
538
+ if (!params.pushdownFilters) throw new Error("dynamic filter delta received without an initial snapshot");
539
+ if (new TextEncoder().encode(encoded).byteLength > (24 << 20)) {
540
+ throw new Error("base64 dynamic filter metadata exceeds the encoded-size limit");
541
+ }
542
+ if (encoded.length % 4 !== 0 ||
543
+ !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded)) {
544
+ throw new Error("dynamic filter metadata is not valid base64");
545
+ }
546
+ const binary = (globalThis as any).atob
547
+ ? (globalThis as any).atob(encoded)
548
+ : Buffer.from(encoded, "base64").toString("binary");
549
+ const bytes = Uint8Array.from(binary, (character: string) => character.charCodeAt(0));
550
+ if (bytes.byteLength > (17 << 20)) throw new Error("dynamic filter IPC payload exceeds the encoded-size limit");
551
+ const current = params.pushdownFilters;
552
+ params.pushdownFilters = current.applyDelta(deserializeBatch(bytes));
553
+ }
554
+
432
555
  /**
433
556
  * Wrap an OutputCollector so a pre-built batch emitted WITH metadata carries
434
557
  * that metadata on the batch object itself (merged over any metadata the
@@ -615,8 +738,12 @@ export interface RowTransformConfig<TArgs = Record<string, any>> {
615
738
  /** Named (string-position) args stay bind-time scalars on `params.args`. */
616
739
  namedArgs?: Record<string, VgiDataType>;
617
740
  argDefaults?: Record<string, any>;
741
+ /** Authoritative typed defaults: one row, defaulted parameters only. */
742
+ parameterDefaultValues?: VgiBatch | null;
618
743
  /** Per-argument descriptions keyed by arg name (surfaced as `vgi_doc`). */
619
744
  argDocs?: Record<string, string>;
745
+ /** Discovery constraints, also enforced for named scalar arguments at bind. */
746
+ argConstraints?: Record<string, ArgumentConstraints>;
620
747
  /** Bind: return the output schema. The input schema (the declared per-row
621
748
  * columns, typed by the C++ bind) is on `params.bindCall.input_schema`. */
622
749
  onBind: (params: TableInOutBindParams<TArgs>) =>
@@ -683,6 +810,7 @@ export function defineRowTransformFunction<
683
810
  position: posIdx++,
684
811
  arrowType: type,
685
812
  doc: config.argDocs?.[name],
813
+ ...constraintSpecFields(config.argConstraints?.[name]),
686
814
  });
687
815
  }
688
816
  }
@@ -693,6 +821,7 @@ export function defineRowTransformFunction<
693
821
  arrowType: config.varargs.type,
694
822
  isVarargs: true,
695
823
  doc: config.varargs.doc ?? config.argDocs?.[config.varargs.name],
824
+ ...constraintSpecFields(config.argConstraints?.[config.varargs.name]),
696
825
  });
697
826
  }
698
827
  if (config.namedArgs) {
@@ -702,6 +831,10 @@ export function defineRowTransformFunction<
702
831
  position: name,
703
832
  arrowType: type,
704
833
  doc: config.argDocs?.[name],
834
+ ...constraintSpecFields(
835
+ config.argConstraints?.[name],
836
+ config.argDefaults?.[name],
837
+ ),
705
838
  });
706
839
  }
707
840
  }
@@ -716,6 +849,7 @@ export function defineRowTransformFunction<
716
849
  examples: config.examples,
717
850
  categories: config.categories,
718
851
  tags: config.tags,
852
+ parameterDefaultValues: config.parameterDefaultValues,
719
853
  maxWorkers: config.maxWorkers,
720
854
  requiredSettings: config.requiredSettings,
721
855
  requiredSecrets: config.requiredSecrets,
@@ -735,7 +869,9 @@ export function defineRowTransformFunction<
735
869
  ? config.argDefaults[spec.name]
736
870
  : undefined;
737
871
  let val = request.arguments.get(spec.position, defaultVal ?? null);
738
- if (typeof val === "bigint") val = Number(val);
872
+ val = narrowArgValue(val);
873
+ const constraints = config.argConstraints?.[spec.name];
874
+ if (constraints) validateConstConstraints(spec.name, constraints, val);
739
875
  args[spec.name] = val;
740
876
  }
741
877
  return args as TArgs;
@@ -755,6 +891,7 @@ export function defineRowTransformFunction<
755
891
  bindCall: request,
756
892
  settings,
757
893
  secrets,
894
+ argumentNames: request.argument_names ?? null,
758
895
  });
759
896
  return {
760
897
  output_schema: result.outputSchema,
@@ -795,9 +932,14 @@ export function defineRowTransformFunction<
795
932
  ? projectSchema(projIds, request.output_schema)
796
933
  : request.output_schema;
797
934
 
798
- const joinKeysLookup = buildJoinKeysLookup(request.join_keys);
799
935
  const pushdownFilters = request.pushdown_filters
800
- ? deserializeFilters(request.pushdown_filters, 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
+ })
801
943
  : undefined;
802
944
 
803
945
  const boundStorage = new BoundStorage(
@@ -824,9 +966,26 @@ export function defineRowTransformFunction<
824
966
  );
825
967
  }
826
968
 
969
+ if (request.bind_call.input_schema == null) {
970
+ // A caller drove this function via table_function() (or the bare
971
+ // producer path) instead of table_in_out_function(input=...) -- a
972
+ // blended row-transform function's positional args ARE its per-row
973
+ // input columns, so it cannot run with no input stream at all. Left
974
+ // unguarded, this doesn't fail here -- it silently degrades to an
975
+ // empty inputSchema, which disables the transport layer's own
976
+ // schema-conformance check downstream and leaves both sides waiting
977
+ // on each other forever: confirmed live, a non-terminating exchange/
978
+ // continuation loop against a real deployed worker, not an error.
979
+ throw new Error(
980
+ `${config.name}: is a table-in-out function (its positional args are ` +
981
+ `per-row input columns) but no input schema was supplied -- call it ` +
982
+ `via table_in_out_function(input=...), not table_function().`,
983
+ );
984
+ }
985
+
827
986
  return {
828
987
  outputSchema,
829
- inputSchema: request.bind_call.input_schema ?? undefined,
988
+ inputSchema: request.bind_call.input_schema,
830
989
  exchangeInit: () => ({ state: null, processParams }),
831
990
  exchangeFn: async (
832
991
  eState: { state: null; processParams: RowTransformProcessParams<TArgs> },
@@ -834,12 +993,14 @@ export function defineRowTransformFunction<
834
993
  out: OutputCollector,
835
994
  ) => {
836
995
  applyRevalidationValidators(eState.processParams, input);
996
+ applyDynamicFilterUpdate(eState.processParams, input);
837
997
  let wrappedOut: OutputCollector = makeMetadataBakingCollector(out);
838
998
  if (projIds) {
839
999
  wrappedOut = makeSchemaReconcilingCollector(wrappedOut, outputSchema);
840
1000
  }
841
- if (config.autoApplyFilters && pushdownFilters) {
842
- 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;
843
1004
  }
844
1005
  await config.process(eState.processParams, input, wrappedOut);
845
1006
  },