@query-farm/vgi 0.6.0

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 (206) hide show
  1. package/LICENSE +134 -0
  2. package/README.md +494 -0
  3. package/dist/__dbg.test-d.d.ts +1 -0
  4. package/dist/arguments/argument-spec.d.ts +51 -0
  5. package/dist/arguments/arguments.d.ts +13 -0
  6. package/dist/arrow/codec/branded.d.ts +83 -0
  7. package/dist/arrow/codec/registry.d.ts +29 -0
  8. package/dist/arrow/codec/repr.d.ts +23 -0
  9. package/dist/arrow/codec/type-descriptors.d.ts +109 -0
  10. package/dist/arrow/impl-arrowjs/build.d.ts +19 -0
  11. package/dist/arrow/impl-arrowjs/canonical.d.ts +15 -0
  12. package/dist/arrow/impl-arrowjs/empty.d.ts +7 -0
  13. package/dist/arrow/impl-arrowjs/filter.d.ts +11 -0
  14. package/dist/arrow/impl-arrowjs/index.d.ts +11 -0
  15. package/dist/arrow/impl-arrowjs/ipc.d.ts +24 -0
  16. package/dist/arrow/impl-arrowjs/iterate.d.ts +39 -0
  17. package/dist/arrow/impl-arrowjs/project.d.ts +10 -0
  18. package/dist/arrow/impl-arrowjs/schema.d.ts +62 -0
  19. package/dist/arrow/impl-arrowjs/statistics.d.ts +13 -0
  20. package/dist/arrow/impl-flechette/build.d.ts +14 -0
  21. package/dist/arrow/impl-flechette/canonical.d.ts +18 -0
  22. package/dist/arrow/impl-flechette/empty.d.ts +9 -0
  23. package/dist/arrow/impl-flechette/filter.d.ts +10 -0
  24. package/dist/arrow/impl-flechette/index.d.ts +11 -0
  25. package/dist/arrow/impl-flechette/ipc.d.ts +33 -0
  26. package/dist/arrow/impl-flechette/iterate.d.ts +29 -0
  27. package/dist/arrow/impl-flechette/message-meta.d.ts +10 -0
  28. package/dist/arrow/impl-flechette/normalize-type.d.ts +11 -0
  29. package/dist/arrow/impl-flechette/project.d.ts +3 -0
  30. package/dist/arrow/impl-flechette/schema.d.ts +47 -0
  31. package/dist/arrow/impl-flechette/statistics.d.ts +13 -0
  32. package/dist/arrow/index.d.ts +11 -0
  33. package/dist/arrow/predicates.d.ts +47 -0
  34. package/dist/arrow/repr.test-d.d.ts +52 -0
  35. package/dist/arrow/schema-types.d.ts +71 -0
  36. package/dist/arrow/types.d.ts +57 -0
  37. package/dist/catalog/attach-option.d.ts +33 -0
  38. package/dist/catalog/attach-options.d.ts +30 -0
  39. package/dist/catalog/composite.d.ts +33 -0
  40. package/dist/catalog/descriptors.d.ts +154 -0
  41. package/dist/catalog/interface.d.ts +239 -0
  42. package/dist/catalog/read-only.d.ts +33 -0
  43. package/dist/client/client.d.ts +189 -0
  44. package/dist/client/errors.d.ts +26 -0
  45. package/dist/client/helpers.d.ts +12 -0
  46. package/dist/client/protocol.d.ts +16 -0
  47. package/dist/client/types.d.ts +125 -0
  48. package/dist/client-entry.d.ts +11 -0
  49. package/dist/client-entry.js +48 -0
  50. package/dist/client-entry.js.map +9 -0
  51. package/dist/codec/asd.d.ts +25 -0
  52. package/dist/crypto.d.ts +36 -0
  53. package/dist/errors.d.ts +24 -0
  54. package/dist/filter-pushdown/collector.d.ts +32 -0
  55. package/dist/filter-pushdown/deserialize.d.ts +16 -0
  56. package/dist/filter-pushdown/evaluate.d.ts +51 -0
  57. package/dist/filter-pushdown/index.d.ts +4 -0
  58. package/dist/filter-pushdown/types.d.ts +84 -0
  59. package/dist/functions/aggregate.d.ts +129 -0
  60. package/dist/functions/copy-from.d.ts +81 -0
  61. package/dist/functions/copy-to.d.ts +121 -0
  62. package/dist/functions/registry.d.ts +16 -0
  63. package/dist/functions/scalar.d.ts +98 -0
  64. package/dist/functions/storage-cf-do.d.ts +47 -0
  65. package/dist/functions/storage.d.ts +86 -0
  66. package/dist/functions/table-buffering.d.ts +99 -0
  67. package/dist/functions/table-in-out.d.ts +64 -0
  68. package/dist/functions/table.d.ts +118 -0
  69. package/dist/functions/types.d.ts +157 -0
  70. package/dist/generated/vgi-client.d.ts +466 -0
  71. package/dist/generated/vgi-protocol-schemas.d.ts +115 -0
  72. package/dist/index.core.d.ts +42 -0
  73. package/dist/index.d.ts +2 -0
  74. package/dist/index.js +10888 -0
  75. package/dist/index.js.map +85 -0
  76. package/dist/metadata/resolve.d.ts +3 -0
  77. package/dist/metadata/serialize.d.ts +12 -0
  78. package/dist/metadata/types.d.ts +47 -0
  79. package/dist/protocol/dispatch.d.ts +23 -0
  80. package/dist/protocol/handlers/aggregate.d.ts +3 -0
  81. package/dist/protocol/handlers/catalog/admin.d.ts +3 -0
  82. package/dist/protocol/handlers/catalog/index.d.ts +3 -0
  83. package/dist/protocol/handlers/catalog/index_methods.d.ts +3 -0
  84. package/dist/protocol/handlers/catalog/macro.d.ts +3 -0
  85. package/dist/protocol/handlers/catalog/shared.d.ts +70 -0
  86. package/dist/protocol/handlers/catalog/table.d.ts +3 -0
  87. package/dist/protocol/handlers/catalog/view.d.ts +3 -0
  88. package/dist/protocol/handlers/function.d.ts +8 -0
  89. package/dist/protocol/handlers/shared.d.ts +31 -0
  90. package/dist/protocol/handlers/table-buffering.d.ts +3 -0
  91. package/dist/protocol/serialize.d.ts +4 -0
  92. package/dist/protocol/serializers/arguments.d.ts +3 -0
  93. package/dist/protocol/serializers/bind.d.ts +9 -0
  94. package/dist/protocol/serializers/cardinality.d.ts +7 -0
  95. package/dist/protocol/serializers/init.d.ts +9 -0
  96. package/dist/protocol/serializers/shared.d.ts +12 -0
  97. package/dist/protocol/state-serializer.d.ts +23 -0
  98. package/dist/protocol/types.d.ts +129 -0
  99. package/dist/secrets/helpers.d.ts +15 -0
  100. package/dist/types.d.ts +52 -0
  101. package/dist/util/arrow/index.d.ts +1 -0
  102. package/dist/util/bytes.d.ts +2 -0
  103. package/dist/util/statistics.d.ts +7 -0
  104. package/dist/worker-cf-entry.d.ts +30 -0
  105. package/dist/worker-cf-entry.js +8767 -0
  106. package/dist/worker-cf-entry.js.map +74 -0
  107. package/dist/worker.d.ts +36 -0
  108. package/package.json +96 -0
  109. package/src/arguments/argument-spec.ts +200 -0
  110. package/src/arguments/arguments.ts +81 -0
  111. package/src/arrow/codec/branded.ts +206 -0
  112. package/src/arrow/codec/registry.ts +674 -0
  113. package/src/arrow/codec/repr.ts +181 -0
  114. package/src/arrow/codec/type-descriptors.ts +137 -0
  115. package/src/arrow/impl-arrowjs/build.ts +88 -0
  116. package/src/arrow/impl-arrowjs/canonical.ts +509 -0
  117. package/src/arrow/impl-arrowjs/empty.ts +72 -0
  118. package/src/arrow/impl-arrowjs/filter.ts +53 -0
  119. package/src/arrow/impl-arrowjs/index.ts +54 -0
  120. package/src/arrow/impl-arrowjs/ipc.ts +67 -0
  121. package/src/arrow/impl-arrowjs/iterate.ts +159 -0
  122. package/src/arrow/impl-arrowjs/project.ts +73 -0
  123. package/src/arrow/impl-arrowjs/schema.ts +256 -0
  124. package/src/arrow/impl-arrowjs/statistics.ts +185 -0
  125. package/src/arrow/impl-flechette/build.ts +65 -0
  126. package/src/arrow/impl-flechette/canonical.ts +289 -0
  127. package/src/arrow/impl-flechette/empty.ts +20 -0
  128. package/src/arrow/impl-flechette/filter.ts +42 -0
  129. package/src/arrow/impl-flechette/index.ts +54 -0
  130. package/src/arrow/impl-flechette/ipc.ts +110 -0
  131. package/src/arrow/impl-flechette/iterate.ts +121 -0
  132. package/src/arrow/impl-flechette/message-meta.ts +175 -0
  133. package/src/arrow/impl-flechette/normalize-type.ts +99 -0
  134. package/src/arrow/impl-flechette/project.ts +64 -0
  135. package/src/arrow/impl-flechette/schema.ts +202 -0
  136. package/src/arrow/impl-flechette/statistics.ts +198 -0
  137. package/src/arrow/index.ts +129 -0
  138. package/src/arrow/predicates.ts +66 -0
  139. package/src/arrow/repr.test-d.ts +189 -0
  140. package/src/arrow/schema-types.ts +213 -0
  141. package/src/arrow/types.ts +74 -0
  142. package/src/catalog/attach-option.ts +107 -0
  143. package/src/catalog/attach-options.ts +79 -0
  144. package/src/catalog/composite.ts +180 -0
  145. package/src/catalog/descriptors.ts +164 -0
  146. package/src/catalog/interface.ts +721 -0
  147. package/src/catalog/read-only.ts +909 -0
  148. package/src/client/client.ts +1348 -0
  149. package/src/client/errors.ts +69 -0
  150. package/src/client/helpers.ts +56 -0
  151. package/src/client/protocol.ts +33 -0
  152. package/src/client/types.ts +140 -0
  153. package/src/client-entry.ts +112 -0
  154. package/src/codec/asd.ts +147 -0
  155. package/src/crypto.ts +143 -0
  156. package/src/errors.ts +61 -0
  157. package/src/filter-pushdown/collector.ts +147 -0
  158. package/src/filter-pushdown/deserialize.ts +253 -0
  159. package/src/filter-pushdown/evaluate.ts +591 -0
  160. package/src/filter-pushdown/index.ts +28 -0
  161. package/src/filter-pushdown/types.ts +91 -0
  162. package/src/functions/aggregate.ts +384 -0
  163. package/src/functions/copy-from.ts +282 -0
  164. package/src/functions/copy-to.ts +319 -0
  165. package/src/functions/registry.ts +258 -0
  166. package/src/functions/scalar.ts +356 -0
  167. package/src/functions/storage-cf-do.ts +303 -0
  168. package/src/functions/storage.ts +521 -0
  169. package/src/functions/table-buffering.ts +562 -0
  170. package/src/functions/table-in-out.ts +373 -0
  171. package/src/functions/table.ts +521 -0
  172. package/src/functions/types.ts +192 -0
  173. package/src/generated/vgi-client.ts +560 -0
  174. package/src/generated/vgi-protocol-schemas.ts +864 -0
  175. package/src/index.core.ts +267 -0
  176. package/src/index.ts +11 -0
  177. package/src/metadata/resolve.ts +109 -0
  178. package/src/metadata/serialize.ts +242 -0
  179. package/src/metadata/types.ts +60 -0
  180. package/src/protocol/dispatch.ts +52 -0
  181. package/src/protocol/handlers/aggregate.ts +355 -0
  182. package/src/protocol/handlers/catalog/admin.ts +368 -0
  183. package/src/protocol/handlers/catalog/index.ts +25 -0
  184. package/src/protocol/handlers/catalog/index_methods.ts +65 -0
  185. package/src/protocol/handlers/catalog/macro.ts +114 -0
  186. package/src/protocol/handlers/catalog/shared.ts +291 -0
  187. package/src/protocol/handlers/catalog/table.ts +401 -0
  188. package/src/protocol/handlers/catalog/view.ts +119 -0
  189. package/src/protocol/handlers/function.ts +302 -0
  190. package/src/protocol/handlers/shared.ts +80 -0
  191. package/src/protocol/handlers/table-buffering.ts +207 -0
  192. package/src/protocol/serialize.ts +23 -0
  193. package/src/protocol/serializers/arguments.ts +160 -0
  194. package/src/protocol/serializers/bind.ts +212 -0
  195. package/src/protocol/serializers/cardinality.ts +45 -0
  196. package/src/protocol/serializers/init.ts +247 -0
  197. package/src/protocol/serializers/shared.ts +46 -0
  198. package/src/protocol/state-serializer.ts +181 -0
  199. package/src/protocol/types.ts +153 -0
  200. package/src/secrets/helpers.ts +70 -0
  201. package/src/types.ts +71 -0
  202. package/src/util/arrow/index.ts +23 -0
  203. package/src/util/bytes.ts +14 -0
  204. package/src/util/statistics.ts +40 -0
  205. package/src/worker-cf-entry.ts +172 -0
  206. package/src/worker.ts +212 -0
@@ -0,0 +1,521 @@
1
+ // Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+ // Table function implementation.
3
+ // Table functions produce output batches from arguments (no streaming input).
4
+
5
+ import {
6
+ type VgiSchema,
7
+ type VgiBatch,
8
+ type VgiDataType,
9
+ isBatch,
10
+ isNull,
11
+ nullType,
12
+ deserializeBatch,
13
+ readCanonicalValue,
14
+ } from "../arrow/index.js";
15
+ import { codecFor } from "../arrow/codec/registry.js";
16
+ import type { OutputCollector } from "@query-farm/vgi-rpc";
17
+ import { DEFAULT_MAX_WORKERS } from "../types.js";
18
+ import type {
19
+ BindRequest,
20
+ BindResponse,
21
+ InitRequest,
22
+ GlobalInitResponse,
23
+ TableFunctionCardinalityRequest,
24
+ TableCardinality,
25
+ } from "../protocol/types.js";
26
+ import type {
27
+ VgiFunction,
28
+ FunctionMeta,
29
+ StreamHandlers,
30
+ FunctionExample,
31
+ } from "./types.js";
32
+ import type { ArgumentSpec } from "../arguments/argument-spec.js";
33
+ import { batchToScalarDict, batchToSecretDict, projectSchema, safeNumber } from "../util/arrow/index.js";
34
+ import {
35
+ buildJoinKeysLookup,
36
+ deserializeFilters,
37
+ FilteringOutputCollector,
38
+ type PushdownFilters,
39
+ } from "../filter-pushdown/index.js";
40
+ import type { ColumnStatistics } from "../util/statistics.js";
41
+ import {
42
+ FunctionStability,
43
+ type NullHandling,
44
+ type OrderPreservation,
45
+ type OrderDependence,
46
+ type DistinctDependence,
47
+ } from "../types.js";
48
+ import { BoundStorage, storage as globalStorage } from "./storage.js";
49
+
50
+ // Base64-decode a string into raw bytes. Used to unpack the dynamic filter
51
+ // update DuckDB attaches to each tick batch's custom metadata.
52
+ function base64Decode(s: string): Uint8Array {
53
+ const bin = (globalThis as any).atob ? (globalThis as any).atob(s) : Buffer.from(s, "base64").toString("binary");
54
+ const out = new Uint8Array(bin.length);
55
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
56
+ return out;
57
+ }
58
+
59
+ // Read the first RecordBatch from an Arrow IPC stream buffer. Returns null if
60
+ // the stream has no batches.
61
+ function deserializeFilterBatch(bytes: Uint8Array): VgiBatch | null {
62
+ if (!bytes || bytes.length === 0) return null;
63
+ const batch = deserializeBatch(bytes);
64
+ return batch.numRows === 0 && batch.schema.fields.length === 0 ? null : batch;
65
+ }
66
+
67
+ // Wrap an OutputCollector so each emitted RecordBatch is projected-by-name
68
+ // to the bound outputSchema before forwarding. Lenient: workers can emit
69
+ // over-wide batches (full declared schema) and the framework drops the
70
+ // columns DuckDB didn't ask for. Field name mismatches yield null columns
71
+ // rather than wrong-position reads. Field types must already match.
72
+ function makeProjectingCollector(inner: OutputCollector, targetSchema: VgiSchema): OutputCollector {
73
+ // Single-pass guard: don't project when a batch already matches.
74
+ function alreadyMatches(batch: VgiBatch): boolean {
75
+ if (batch.schema.fields.length !== targetSchema.fields.length) return false;
76
+ for (let i = 0; i < targetSchema.fields.length; i++) {
77
+ if (batch.schema.fields[i].name !== targetSchema.fields[i].name) return false;
78
+ }
79
+ return true;
80
+ }
81
+ function project(batch: VgiBatch): VgiBatch {
82
+ if (alreadyMatches(batch)) return batch;
83
+ const { batchFromColumns } = require("../arrow/index.js");
84
+ const cols: Record<string, any[]> = {};
85
+ for (const f of targetSchema.fields) {
86
+ const src = batch.getChild(f.name);
87
+ if (src) {
88
+ // Canonical read -> rich so the rebuild goes through the codec path
89
+ // (backend-agnostic, lossless) rather than the raw `.get(i)`.
90
+ const type = f.type as VgiDataType;
91
+ const codec = codecFor(type);
92
+ const arr: any[] = [];
93
+ for (let i = 0; i < batch.numRows; i++) {
94
+ arr.push(codec.canonicalToRich(readCanonicalValue(type, src, i)));
95
+ }
96
+ cols[f.name] = arr;
97
+ } else {
98
+ cols[f.name] = new Array(batch.numRows).fill(null);
99
+ }
100
+ }
101
+ return batchFromColumns(cols, targetSchema);
102
+ }
103
+ return new Proxy(inner, {
104
+ get(target, prop, receiver) {
105
+ if (prop === "emit") {
106
+ return function (this: OutputCollector, batchOrColumns: VgiBatch | Record<string, any[]>, metadata?: Map<string, string>) {
107
+ if (isBatch(batchOrColumns)) {
108
+ return (target as any).emit(project(batchOrColumns), metadata);
109
+ }
110
+ // Object form: vgi-rpc itself converts to a batch via outputSchema,
111
+ // and the user passed columns by name — already aligned.
112
+ return (target as any).emit(batchOrColumns, metadata);
113
+ };
114
+ }
115
+ const v = Reflect.get(target, prop, receiver);
116
+ return typeof v === "function" ? v.bind(target) : v;
117
+ },
118
+ });
119
+ }
120
+
121
+ // ============================================================================
122
+ // Table function parameter bundles
123
+ // ============================================================================
124
+
125
+ export interface TableBindParams<TArgs = Record<string, any>> {
126
+ args: TArgs;
127
+ bindCall: BindRequest;
128
+ settings: Record<string, any>;
129
+ secrets: Record<string, Record<string, any>>;
130
+ resolvedSecretsProvided: boolean;
131
+ }
132
+
133
+ export interface TableProcessParams<TArgs = Record<string, any>> {
134
+ args: TArgs;
135
+ initCall: InitRequest;
136
+ initResponse: GlobalInitResponse;
137
+ outputSchema: VgiSchema;
138
+ settings: Record<string, any>;
139
+ secrets: Record<string, Record<string, any>>;
140
+ pushdownFilters?: PushdownFilters;
141
+ storage?: BoundStorage;
142
+ /**
143
+ * AT (TIMESTAMP|VERSION) clause for this scan, or `undefined` when the scan
144
+ * has no AT clause. Carried on the per-scan bind embedded in the init request
145
+ * (`initCall.bind_call.at_unit` / `.at_value`), so function-backed tables can
146
+ * read time travel at init alongside their pushdown filters. Mirrors
147
+ * vgi-python's `ProcessParams.at_unit` / `.at_value`. See `BindRequest.at_unit`.
148
+ */
149
+ atUnit?: string;
150
+ atValue?: string;
151
+ }
152
+
153
+ // ============================================================================
154
+ // Functional API
155
+ // ============================================================================
156
+
157
+ export interface TableFunctionConfig<
158
+ TArgs = Record<string, any>,
159
+ TState = null,
160
+ > {
161
+ name: string;
162
+ description?: string;
163
+ /** Argument schema (positional args) */
164
+ args?: Record<string, VgiDataType>;
165
+ /** Argument docs */
166
+ argDocs?: Record<string, string>;
167
+ /** Argument defaults */
168
+ argDefaults?: Record<string, any>;
169
+ /** Names of args that accept variable number of arguments */
170
+ varargs?: string[];
171
+ /** Bind: return output schema. May be async — handlers `await` the result. */
172
+ onBind: (params: TableBindParams<TArgs>) =>
173
+ | {
174
+ outputSchema: VgiSchema;
175
+ opaqueData?: Uint8Array;
176
+ lookupSecretTypes?: string[];
177
+ lookupScopes?: string[];
178
+ lookupNames?: string[];
179
+ }
180
+ | Promise<{
181
+ outputSchema: VgiSchema;
182
+ opaqueData?: Uint8Array;
183
+ lookupSecretTypes?: string[];
184
+ lookupScopes?: string[];
185
+ lookupNames?: string[];
186
+ }>;
187
+ /** Init (optional). May be async — common when storage is HTTP-backed. */
188
+ onInit?: (params: {
189
+ args: TArgs;
190
+ initCall: InitRequest;
191
+ outputSchema: VgiSchema;
192
+ executionId: Uint8Array;
193
+ storage: BoundStorage;
194
+ }) => GlobalInitResponse | Promise<GlobalInitResponse>;
195
+ /** State factory */
196
+ initialState?: (params: TableProcessParams<TArgs>) => TState;
197
+ /** Process: emit batches via out, call out.finish() when done */
198
+ process: (
199
+ params: TableProcessParams<TArgs>,
200
+ state: TState,
201
+ out: OutputCollector
202
+ ) => void | Promise<void>;
203
+ /** Cardinality hints */
204
+ cardinality?: (params: TableBindParams<TArgs>) => TableCardinality | Promise<TableCardinality>;
205
+ /**
206
+ * Per-column statistics for the function's output. Returned to DuckDB via
207
+ * the `table_function_statistics` RPC; the optimizer uses min/max to
208
+ * eliminate impossible filters at plan time (folding scans to
209
+ * EMPTY_RESULT). Return `null` or an empty array when bounds are unknown.
210
+ */
211
+ statistics?: (params: TableBindParams<TArgs>) => ColumnStatistics[] | null;
212
+ /**
213
+ * Per-execution diagnostics surfaced under EXPLAIN ANALYZE. DuckDB calls
214
+ * this once per parallel scan thread at pipeline FinishSource via the
215
+ * `table_function_dynamic_to_string` RPC. Return ordered key→value
216
+ * strings; the C++ extension merges these with the intrinsic keys
217
+ * (Function, Rows Read, Threads). The framework provides a BoundStorage
218
+ * keyed by the global execution_id so process() can persist counters
219
+ * that this callback then reads back — see profiling_demo for the
220
+ * canonical pattern.
221
+ */
222
+ dynamicToString?: (
223
+ params: TableBindParams<TArgs>,
224
+ executionId: Uint8Array,
225
+ storage: BoundStorage,
226
+ ) => Record<string, string> | Promise<Record<string, string>>;
227
+ // Metadata
228
+ projectionPushdown?: boolean;
229
+ filterPushdown?: boolean;
230
+ samplingPushdown?: boolean;
231
+ /** Opt in to DuckDB's late-materialization SEMI-join rewrite; requires a
232
+ * UNIQUE, snapshot-stable rowid column. FunctionInfo.late_materialization. */
233
+ lateMaterialization?: boolean;
234
+ supportedExpressionFilters?: string[];
235
+ autoApplyFilters?: boolean;
236
+ stability?: FunctionStability;
237
+ examples?: FunctionExample[];
238
+ categories?: string[];
239
+ tags?: Record<string, string>;
240
+ maxWorkers?: number;
241
+ requiredSettings?: string[];
242
+ requiredSecrets?: string[];
243
+ /** Row-order preservation behavior; flows to DuckDB's
244
+ * TableFunction::order_preservation_type. */
245
+ preservesOrder?: OrderPreservation;
246
+ nullHandling?: NullHandling;
247
+ orderDependent?: OrderDependence;
248
+ distinctDependent?: DistinctDependence;
249
+ /** Emit per-batch `vgi_batch_index`; FunctionInfo.supports_batch_index. */
250
+ supportsBatchIndex?: boolean;
251
+ /** Hive-style partition-columns mode; FunctionInfo.partition_kind. */
252
+ partitionKind?: "NOT_PARTITIONED" | "SINGLE_VALUE_PARTITIONS" | "OVERLAPPING_PARTITIONS" | "DISJOINT_PARTITIONS";
253
+ }
254
+
255
+ export function defineTableFunction<
256
+ TArgs = Record<string, any>,
257
+ TState = null,
258
+ >(config: TableFunctionConfig<TArgs, TState>): VgiFunction {
259
+ // Build argument specs
260
+ const specs: ArgumentSpec[] = [];
261
+ let posIdx = 0;
262
+
263
+ if (config.args) {
264
+ const varargsSet = new Set(config.varargs ?? []);
265
+ for (const [name, type] of Object.entries(config.args)) {
266
+ const isAny = isNull(type);
267
+ const hasDefault = config.argDefaults?.[name] !== undefined;
268
+ specs.push({
269
+ name,
270
+ // Args with defaults are named (string position), others are positional
271
+ position: hasDefault ? name : posIdx++,
272
+ arrowType: isAny ? nullType() : type,
273
+ isAnyType: isAny,
274
+ isVarargs: varargsSet.has(name),
275
+ doc: config.argDocs?.[name],
276
+ });
277
+ }
278
+ }
279
+
280
+ const meta: FunctionMeta = {
281
+ name: config.name,
282
+ description: config.description,
283
+ stability: config.stability,
284
+ projectionPushdown: config.projectionPushdown,
285
+ filterPushdown: config.filterPushdown,
286
+ samplingPushdown: config.samplingPushdown,
287
+ lateMaterialization: config.lateMaterialization,
288
+ supportedExpressionFilters: config.supportedExpressionFilters,
289
+ autoApplyFilters: config.autoApplyFilters,
290
+ examples: config.examples,
291
+ categories: config.categories,
292
+ tags: config.tags,
293
+ maxWorkers: config.maxWorkers,
294
+ requiredSettings: config.requiredSettings,
295
+ requiredSecrets: config.requiredSecrets,
296
+ preservesOrder: config.preservesOrder,
297
+ nullHandling: config.nullHandling,
298
+ orderDependent: config.orderDependent,
299
+ distinctDependent: config.distinctDependent,
300
+ supportsBatchIndex: config.supportsBatchIndex,
301
+ partitionKind: config.partitionKind,
302
+ };
303
+
304
+ function extractArgs(request: BindRequest): TArgs {
305
+ const args: Record<string, any> = {};
306
+ for (const spec of specs) {
307
+ const defaultVal =
308
+ config.argDefaults?.[spec.name] !== undefined
309
+ ? config.argDefaults[spec.name]
310
+ : undefined;
311
+ let val: any;
312
+ try {
313
+ val = request.arguments.get(spec.position, defaultVal);
314
+ } catch {
315
+ // Fallback: try by name (for scan function tables where DuckDB
316
+ // converts positional args to named args)
317
+ val = request.arguments.get(spec.name, defaultVal);
318
+ }
319
+ // Arrow Int64 values come through as BigInt — coerce to number
320
+ if (typeof val === "bigint") val = safeNumber(val);
321
+ args[spec.name] = val;
322
+ }
323
+ return args as TArgs;
324
+ }
325
+
326
+ return {
327
+ kind: "table",
328
+ meta,
329
+ argumentSpecs: specs,
330
+
331
+ async bind(request: BindRequest): Promise<BindResponse> {
332
+ const args = extractArgs(request);
333
+ const settings = batchToScalarDict(request.settings);
334
+ const secrets = batchToSecretDict(request.secrets);
335
+ const result = await config.onBind({
336
+ args, bindCall: request, settings, secrets,
337
+ resolvedSecretsProvided: request.resolved_secrets_provided ?? false,
338
+ });
339
+ return {
340
+ output_schema: result.outputSchema,
341
+ opaque_data: result.opaqueData ?? null,
342
+ lookup_secret_types: result.lookupSecretTypes,
343
+ lookup_scopes: result.lookupScopes,
344
+ lookup_names: result.lookupNames,
345
+ };
346
+ },
347
+
348
+ async globalInit(request: InitRequest): Promise<GlobalInitResponse> {
349
+ const executionId = new Uint8Array(16);
350
+ crypto.getRandomValues(executionId);
351
+
352
+ if (request.execution_id) {
353
+ // Secondary init - reuse execution ID
354
+ return {
355
+ max_workers: config.maxWorkers ?? 1,
356
+ execution_id: request.execution_id,
357
+ opaque_data: null,
358
+ };
359
+ }
360
+
361
+ if (config.onInit) {
362
+ const args = extractArgs(request.bind_call);
363
+ const boundStorage = new BoundStorage(globalStorage, executionId);
364
+ return await config.onInit({
365
+ args,
366
+ initCall: request,
367
+ outputSchema: request.output_schema,
368
+ executionId,
369
+ storage: boundStorage,
370
+ });
371
+ }
372
+
373
+ return {
374
+ max_workers: config.maxWorkers ?? 1,
375
+ execution_id: executionId,
376
+ opaque_data: null,
377
+ };
378
+ },
379
+
380
+ createStreamHandlers(
381
+ request: InitRequest,
382
+ response: GlobalInitResponse
383
+ ): StreamHandlers {
384
+ const args = extractArgs(request.bind_call);
385
+ const settings = batchToScalarDict(request.bind_call.settings);
386
+ const secrets = batchToSecretDict(request.bind_call.secrets);
387
+
388
+ // Apply projection pushdown only if the function supports it
389
+ const projIds = request.projection_ids && meta.projectionPushdown
390
+ ? request.projection_ids
391
+ : null;
392
+ const outputSchema = projIds
393
+ ? projectSchema(projIds, request.output_schema)
394
+ : request.output_schema;
395
+
396
+ // Deserialize pushdown filters. Pass a join-keys column lookup so that
397
+ // filters DuckDB promoted to join_keys (IN/OR lists, etc.) are
398
+ // materialized as InFilters rather than silently dropped.
399
+ const joinKeysLookup = buildJoinKeysLookup(request.join_keys);
400
+ const pushdownFilters = request.pushdown_filters
401
+ ? deserializeFilters(request.pushdown_filters, joinKeysLookup)
402
+ : undefined;
403
+
404
+ const boundStorage = new BoundStorage(globalStorage, response.execution_id);
405
+
406
+ const processParams: TableProcessParams<TArgs> = {
407
+ args,
408
+ initCall: request,
409
+ initResponse: response,
410
+ outputSchema,
411
+ settings,
412
+ secrets,
413
+ pushdownFilters,
414
+ storage: boundStorage,
415
+ atUnit: request.bind_call.at_unit ?? undefined,
416
+ atValue: request.bind_call.at_value ?? undefined,
417
+ };
418
+
419
+ const state = config.initialState
420
+ ? config.initialState(processParams)
421
+ : (null as TState);
422
+
423
+ return {
424
+ outputSchema,
425
+ producerInit: () => ({ state, processParams }),
426
+ onTick: (
427
+ pState: { state: TState; processParams: TableProcessParams<TArgs> },
428
+ tickMetadata: Map<string, string> | undefined,
429
+ ) => {
430
+ // Dynamic filter pushdown: DuckDB's Top-N optimizer tightens filters
431
+ // between ticks and serializes the current filter into the tick
432
+ // batch's custom metadata under `vgi_pushdown_filters` (base64 of a
433
+ // filter IPC stream). Decode and overwrite the current pushdown
434
+ // filters so process() sees the updated value.
435
+ if (!tickMetadata) return;
436
+ const encoded = tickMetadata.get("vgi_pushdown_filters");
437
+ if (!encoded) return;
438
+ try {
439
+ const bytes = base64Decode(encoded);
440
+ const filterBatch = deserializeFilterBatch(bytes);
441
+ if (filterBatch) {
442
+ const updated = deserializeFilters(filterBatch, joinKeysLookup);
443
+ pState.processParams.pushdownFilters = updated;
444
+ }
445
+ } catch {
446
+ // Malformed dynamic-filter update: keep the previous filter. Not
447
+ // fatal — this is a best-effort optimization hint from DuckDB.
448
+ }
449
+ },
450
+ producerFn: async (
451
+ pState: { state: TState; processParams: TableProcessParams<TArgs> },
452
+ out: OutputCollector
453
+ ) => {
454
+ const current = pState.processParams.pushdownFilters;
455
+ // Auto-project: workers may emit a batch with the function's full
456
+ // declared schema even when DuckDB only requested a subset. Wrap
457
+ // the OutputCollector so each emit() projects-by-name to the
458
+ // bound outputSchema. Lenient: absent fields become null columns.
459
+ let wrappedOut: OutputCollector =
460
+ outputSchema && projIds
461
+ ? makeProjectingCollector(out, outputSchema)
462
+ : out;
463
+ if (config.autoApplyFilters && current) {
464
+ wrappedOut = new FilteringOutputCollector(wrappedOut, current) as unknown as OutputCollector;
465
+ }
466
+ await config.process(pState.processParams, pState.state, wrappedOut);
467
+ },
468
+ };
469
+ },
470
+
471
+ cardinality: config.cardinality
472
+ ? (request: TableFunctionCardinalityRequest) => {
473
+ const args = extractArgs(request.bind_call);
474
+ const settings = batchToScalarDict(request.bind_call.settings);
475
+ const secrets = batchToSecretDict(request.bind_call.secrets);
476
+ return config.cardinality!({
477
+ args,
478
+ bindCall: request.bind_call,
479
+ settings,
480
+ secrets,
481
+ resolvedSecretsProvided: request.bind_call.resolved_secrets_provided ?? false,
482
+ });
483
+ }
484
+ : undefined,
485
+
486
+ statistics: config.statistics
487
+ ? (request: TableFunctionCardinalityRequest) => {
488
+ const args = extractArgs(request.bind_call);
489
+ const settings = batchToScalarDict(request.bind_call.settings);
490
+ const secrets = batchToSecretDict(request.bind_call.secrets);
491
+ return config.statistics!({
492
+ args,
493
+ bindCall: request.bind_call,
494
+ settings,
495
+ secrets,
496
+ resolvedSecretsProvided: request.bind_call.resolved_secrets_provided ?? false,
497
+ });
498
+ }
499
+ : undefined,
500
+
501
+ dynamicToString: config.dynamicToString
502
+ ? (request) => {
503
+ const args = extractArgs(request.bindCall);
504
+ const settings = batchToScalarDict(request.bindCall.settings);
505
+ const secrets = batchToSecretDict(request.bindCall.secrets);
506
+ const storage = new BoundStorage(globalStorage, request.globalExecutionId);
507
+ return config.dynamicToString!(
508
+ {
509
+ args,
510
+ bindCall: request.bindCall,
511
+ settings,
512
+ secrets,
513
+ resolvedSecretsProvided: request.bindCall.resolved_secrets_provided ?? false,
514
+ },
515
+ request.globalExecutionId,
516
+ storage,
517
+ );
518
+ }
519
+ : undefined,
520
+ };
521
+ }