@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,373 @@
1
+ // Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+ // Table In-Out function implementation.
3
+ // Two-phase: INPUT phase receives and transforms batches,
4
+ // FINALIZE phase emits final results.
5
+
6
+ import { type VgiSchema, schema, type VgiField, type VgiDataType, type VgiBatch, nullType } from "../arrow/index.js";
7
+ import type { OutputCollector } from "@query-farm/vgi-rpc";
8
+ import { DEFAULT_MAX_WORKERS, TableInOutPhase } from "../types.js";
9
+ import type {
10
+ BindRequest,
11
+ BindResponse,
12
+ InitRequest,
13
+ GlobalInitResponse,
14
+ } from "../protocol/types.js";
15
+ import type {
16
+ VgiFunction,
17
+ FunctionMeta,
18
+ StreamHandlers,
19
+ FunctionExample,
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";
23
+ import {
24
+ buildJoinKeysLookup,
25
+ deserializeFilters,
26
+ FilteringOutputCollector,
27
+ type PushdownFilters,
28
+ } from "../filter-pushdown/index.js";
29
+ import { FunctionStability } from "../types.js";
30
+ import { BoundStorage, storage as defaultStorage } from "./storage.js";
31
+ import { serializeUserState, deserializeUserState } from "../protocol/state-serializer.js";
32
+
33
+ // ============================================================================
34
+ // Table In-Out parameter bundles (reuse table function's)
35
+ // ============================================================================
36
+
37
+ export interface TableInOutBindParams<TArgs = Record<string, any>> {
38
+ args: TArgs;
39
+ bindCall: BindRequest;
40
+ settings: Record<string, any>;
41
+ secrets: Record<string, Record<string, any>>;
42
+ }
43
+
44
+ export interface TableInOutProcessParams<TArgs = Record<string, any>> {
45
+ args: TArgs;
46
+ initCall: InitRequest;
47
+ initResponse: GlobalInitResponse;
48
+ outputSchema: VgiSchema;
49
+ settings: Record<string, any>;
50
+ secrets: Record<string, Record<string, any>>;
51
+ pushdownFilters?: PushdownFilters;
52
+ /** Shared storage for cross-phase and cross-worker data (SQLite-backed). */
53
+ storage: BoundStorage;
54
+ }
55
+
56
+ // ============================================================================
57
+ // Functional API
58
+ // ============================================================================
59
+
60
+ export interface TableInOutConfig<
61
+ TArgs = Record<string, any>,
62
+ TState = null,
63
+ > {
64
+ name: string;
65
+ description?: string;
66
+ args?: Record<string, VgiDataType>;
67
+ /** Named arguments (optional, DuckDB passes by name) */
68
+ namedArgs?: Record<string, VgiDataType>;
69
+ /** Argument defaults */
70
+ argDefaults?: Record<string, any>;
71
+ /** Bind: default passes through input schema. May be async. */
72
+ onBind?: (params: TableInOutBindParams<TArgs>) =>
73
+ | { outputSchema: VgiSchema; opaqueData?: Uint8Array }
74
+ | Promise<{ outputSchema: VgiSchema; opaqueData?: Uint8Array }>;
75
+ onInit?: (params: {
76
+ args: TArgs;
77
+ initCall: InitRequest;
78
+ outputSchema: VgiSchema;
79
+ executionId: Uint8Array;
80
+ }) => GlobalInitResponse | Promise<GlobalInitResponse>;
81
+ initialState?: (params: TableInOutProcessParams<TArgs>) => TState;
82
+ /** Process: transform input batch, emit output via out */
83
+ process?: (
84
+ params: TableInOutProcessParams<TArgs>,
85
+ state: TState,
86
+ batch: VgiBatch,
87
+ out: OutputCollector
88
+ ) => void | Promise<void>;
89
+ /** Finalize: emit final batches after all input processed.
90
+ * Receives all worker states collected from storage (matches Python's finish(params, states)). */
91
+ finalize?: (
92
+ params: TableInOutProcessParams<TArgs>,
93
+ states: TState[]
94
+ ) => VgiBatch[] | Promise<VgiBatch[]>;
95
+ // Metadata
96
+ projectionPushdown?: boolean;
97
+ filterPushdown?: boolean;
98
+ autoApplyFilters?: boolean;
99
+ stability?: FunctionStability;
100
+ examples?: FunctionExample[];
101
+ categories?: string[];
102
+ tags?: Record<string, string>;
103
+ maxWorkers?: number;
104
+ requiredSettings?: string[];
105
+ requiredSecrets?: string[];
106
+ }
107
+
108
+ export function defineTableInOutFunction<
109
+ TArgs = Record<string, any>,
110
+ TState = null,
111
+ >(config: TableInOutConfig<TArgs, TState>): VgiFunction {
112
+
113
+ // Build argument specs
114
+ const specs: ArgumentSpec[] = [];
115
+ let posIdx = 0;
116
+
117
+ // Positional args come before the table input
118
+ if (config.args) {
119
+ for (const [name, type] of Object.entries(config.args)) {
120
+ specs.push({
121
+ name,
122
+ position: posIdx++,
123
+ arrowType: type,
124
+ });
125
+ }
126
+ }
127
+
128
+ // Table-in-out always has a table input (after positional args)
129
+ specs.push({
130
+ name: "data",
131
+ position: posIdx++,
132
+ arrowType: nullType(),
133
+ isTableInput: true,
134
+ });
135
+
136
+ if (config.namedArgs) {
137
+ for (const [name, type] of Object.entries(config.namedArgs)) {
138
+ specs.push({
139
+ name,
140
+ position: name, // string position = named arg
141
+ arrowType: type,
142
+ });
143
+ }
144
+ }
145
+
146
+ const meta: FunctionMeta = {
147
+ name: config.name,
148
+ description: config.description,
149
+ stability: config.stability,
150
+ projectionPushdown: config.projectionPushdown,
151
+ filterPushdown: config.filterPushdown,
152
+ autoApplyFilters: config.autoApplyFilters,
153
+ examples: config.examples,
154
+ categories: config.categories,
155
+ tags: config.tags,
156
+ maxWorkers: config.maxWorkers,
157
+ requiredSettings: config.requiredSettings,
158
+ requiredSecrets: config.requiredSecrets,
159
+ hasFinalize: !!config.finalize,
160
+ };
161
+
162
+ function extractArgs(request: BindRequest): TArgs {
163
+ const args: Record<string, any> = {};
164
+ for (const spec of specs) {
165
+ if (spec.isTableInput) continue;
166
+ const defaultVal =
167
+ config.argDefaults?.[spec.name] !== undefined
168
+ ? config.argDefaults[spec.name]
169
+ : undefined;
170
+ // For named args, position is a string (the arg name)
171
+ let val = request.arguments.get(spec.position, defaultVal);
172
+ // Arrow Int64 values come through as BigInt — coerce to number
173
+ if (typeof val === "bigint") val = Number(val);
174
+ args[spec.name] = val;
175
+ }
176
+ return args as TArgs;
177
+ }
178
+
179
+ return {
180
+ kind: "table_in_out",
181
+ meta,
182
+ argumentSpecs: specs,
183
+
184
+ async bind(request: BindRequest): Promise<BindResponse> {
185
+ if (config.onBind) {
186
+ const args = extractArgs(request);
187
+ const settings = batchToScalarDict(request.settings);
188
+ const secrets = batchToSecretDict(request.secrets);
189
+ const result = await config.onBind({
190
+ args,
191
+ bindCall: request,
192
+ settings,
193
+ secrets,
194
+ });
195
+ return {
196
+ output_schema: result.outputSchema,
197
+ opaque_data: result.opaqueData ?? null,
198
+ };
199
+ }
200
+ // Default: pass through input schema
201
+ return {
202
+ output_schema: request.input_schema ?? schema([]),
203
+ opaque_data: null,
204
+ };
205
+ },
206
+
207
+ async globalInit(request: InitRequest): Promise<GlobalInitResponse> {
208
+ const executionId = new Uint8Array(16);
209
+ crypto.getRandomValues(executionId);
210
+
211
+ if (request.execution_id) {
212
+ return {
213
+ max_workers: DEFAULT_MAX_WORKERS,
214
+ execution_id: request.execution_id,
215
+ opaque_data: null,
216
+ };
217
+ }
218
+
219
+ if (config.onInit) {
220
+ const args = extractArgs(request.bind_call);
221
+ return await config.onInit({
222
+ args,
223
+ initCall: request,
224
+ outputSchema: request.output_schema,
225
+ executionId,
226
+ });
227
+ }
228
+
229
+ return {
230
+ max_workers: config.maxWorkers ?? 1,
231
+ execution_id: executionId,
232
+ opaque_data: null,
233
+ };
234
+ },
235
+
236
+ createStreamHandlers(
237
+ request: InitRequest,
238
+ response: GlobalInitResponse,
239
+ accumulatedState?: any,
240
+ ): StreamHandlers {
241
+ const args = extractArgs(request.bind_call);
242
+ const settings = batchToScalarDict(request.bind_call.settings);
243
+ const secrets = batchToSecretDict(request.bind_call.secrets);
244
+
245
+ // Apply projection pushdown only if the function supports it
246
+ const projIds = request.projection_ids && meta.projectionPushdown
247
+ ? request.projection_ids
248
+ : null;
249
+ const outputSchema = projIds
250
+ ? projectSchema(projIds, request.output_schema)
251
+ : request.output_schema;
252
+
253
+ // Deserialize pushdown filters. Pass a join-keys column lookup so that
254
+ // filters DuckDB promoted to join_keys (IN/OR lists, etc.) are
255
+ // materialized as InFilters rather than silently dropped.
256
+ const joinKeysLookup = buildJoinKeysLookup(request.join_keys);
257
+ const pushdownFilters = request.pushdown_filters
258
+ ? deserializeFilters(request.pushdown_filters, joinKeysLookup)
259
+ : undefined;
260
+
261
+ // Create BoundStorage for cross-phase/cross-worker data sharing
262
+ const boundStorage = new BoundStorage(
263
+ defaultStorage,
264
+ response.execution_id ?? new Uint8Array(16),
265
+ );
266
+
267
+ const processParams: TableInOutProcessParams<TArgs> = {
268
+ args,
269
+ initCall: request,
270
+ initResponse: response,
271
+ outputSchema,
272
+ settings,
273
+ secrets,
274
+ pushdownFilters,
275
+ storage: boundStorage,
276
+ };
277
+
278
+ const phase = request.phase;
279
+
280
+ if (phase === TableInOutPhase.FINALIZE) {
281
+ // FINALIZE phase: producer mode.
282
+ // The actual collect+finalize work is deferred into the first
283
+ // producerFn call so the (now-async) FunctionStorage can be awaited
284
+ // there. Storing the materialized batches on the per-call state.
285
+ return {
286
+ outputSchema,
287
+ producerInit: () => ({ state: { batchIdx: 0 }, batches: null as VgiBatch[] | null }),
288
+ producerFn: async (
289
+ pState: { state: { batchIdx: number }; batches: VgiBatch[] | null },
290
+ out: OutputCollector
291
+ ) => {
292
+ if (pState.batches == null) {
293
+ const finalizeStates: TState[] = [];
294
+ if (accumulatedState != null) {
295
+ finalizeStates.push(accumulatedState as TState);
296
+ }
297
+ // Collect any states persisted by INPUT exchanges. Subprocess
298
+ // path reads from SQLite; HTTP path reads from CF DO / wherever
299
+ // the configured storage backend lives.
300
+ const stored = await boundStorage.collect();
301
+ for (const bytes of stored) {
302
+ finalizeStates.push(deserializeUserState(bytes) as TState);
303
+ }
304
+ if (finalizeStates.length === 0) {
305
+ finalizeStates.push(
306
+ config.initialState
307
+ ? config.initialState(processParams)
308
+ : (null as TState),
309
+ );
310
+ }
311
+ pState.batches = config.finalize
312
+ ? await config.finalize(processParams, finalizeStates)
313
+ : [];
314
+ }
315
+ if (pState.state.batchIdx >= pState.batches.length) {
316
+ out.finish();
317
+ return;
318
+ }
319
+ out.emit(pState.batches[pState.state.batchIdx]);
320
+ pState.state.batchIdx++;
321
+ if (pState.state.batchIdx >= pState.batches.length) {
322
+ out.finish();
323
+ }
324
+ },
325
+ };
326
+ }
327
+
328
+ // INPUT phase: exchange mode
329
+ const state = config.initialState
330
+ ? config.initialState(processParams)
331
+ : (null as TState);
332
+
333
+ const processFn =
334
+ config.process ??
335
+ ((_params: any, _state: any, batch: VgiBatch, out: OutputCollector) => {
336
+ // Default passthrough. When DuckDB pushed a projection, the declared
337
+ // output schema is narrowed to `projIds`, so the emitted batch must be
338
+ // narrowed too — otherwise it carries columns (and their dictionaries)
339
+ // the stream schema doesn't declare, and the C++ reader rejects the
340
+ // batch (e.g. "No record of dictionary type with id 1" when the
341
+ // dropped columns were dictionary-encoded ENUMs).
342
+ out.emit(projIds ? projectBatch(projIds, batch) : batch);
343
+ });
344
+
345
+ return {
346
+ outputSchema,
347
+ inputSchema: request.bind_call.input_schema ?? undefined,
348
+ exchangeInit: () => ({ state, processParams }),
349
+ exchangeFn: async (
350
+ eState: {
351
+ state: TState;
352
+ processParams: TableInOutProcessParams<TArgs>;
353
+ },
354
+ input: VgiBatch,
355
+ out: OutputCollector
356
+ ) => {
357
+ const wrappedOut = (config.autoApplyFilters && pushdownFilters)
358
+ ? new FilteringOutputCollector(out, pushdownFilters) as unknown as OutputCollector
359
+ : out;
360
+ await processFn(eState.processParams, eState.state, input, wrappedOut);
361
+
362
+ // Auto-persist accumulated state for FINALIZE recovery (matches Python/Go pattern)
363
+ if (eState.state != null) {
364
+ const serialized = serializeUserState(eState.state);
365
+ if (serialized != null) {
366
+ await boundStorage.put(serialized);
367
+ }
368
+ }
369
+ },
370
+ };
371
+ },
372
+ };
373
+ }