@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,119 @@
1
+ // Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+ // Catalog view handlers: view_get/create/drop/rename/comment_set.
3
+
4
+ import { type VgiSchema, schema, type VgiField, field, type VgiDataType, binary, utf8 } from "../../../arrow/index.js";
5
+ import { Protocol } from "@query-farm/vgi-rpc";
6
+ import { encodeViewInfo } from "../../../generated/vgi-client.js";
7
+ import { CatalogViewGetResultSchema } from "../../../generated/vgi-protocol-schemas.js";
8
+ import { toUint8Array } from "../../../util/bytes.js";
9
+ import {
10
+ RESULT_BINARY_SCHEMA,
11
+ wrapResult,
12
+ } from "../shared.js";
13
+ import {
14
+ type GetCatalog,
15
+ emptyResultSchema,
16
+ attachOpaqueDataSchemaNameTxnParams,
17
+ schemaNameIgnoreNotFoundTxnParams,
18
+ schemaNameRenameParams,
19
+ schemaNameCommentParams,
20
+ catalogUnary,
21
+ } from "./shared.js";
22
+
23
+ export function registerCatalogViewMethods(protocol: Protocol, getCatalog: GetCatalog, signingKey?: Uint8Array): void {
24
+ // catalog_view_get
25
+ catalogUnary(protocol, signingKey, "catalog_view_get", {
26
+ params: attachOpaqueDataSchemaNameTxnParams,
27
+ result: RESULT_BINARY_SCHEMA,
28
+ handler: async (params) => {
29
+ const cat = getCatalog();
30
+ const info = await cat.viewGet(
31
+ toUint8Array(params.attach_opaque_data),
32
+ params.schema_name,
33
+ params.name,
34
+ params.transaction_opaque_data ? toUint8Array(params.transaction_opaque_data) : undefined
35
+ );
36
+ return wrapResult({
37
+ items: info ? [encodeViewInfo(info)] : [],
38
+ }, CatalogViewGetResultSchema);
39
+ },
40
+ });
41
+
42
+ // catalog_view_create
43
+ catalogUnary(protocol, signingKey, "catalog_view_create", {
44
+ params: schema([
45
+ field("attach_opaque_data", binary(), true),
46
+ field("schema_name", utf8(), false),
47
+ field("name", utf8(), false),
48
+ field("definition", utf8(), false),
49
+ field("on_conflict", utf8(), false),
50
+ field("transaction_opaque_data", binary(), true),
51
+ ]),
52
+ result: emptyResultSchema,
53
+ handler: async (params) => {
54
+ const cat = getCatalog();
55
+ await cat.viewCreate(
56
+ toUint8Array(params.attach_opaque_data),
57
+ params.schema_name,
58
+ params.name,
59
+ params.definition,
60
+ params.on_conflict,
61
+ params.transaction_opaque_data ? toUint8Array(params.transaction_opaque_data) : undefined
62
+ );
63
+ return {};
64
+ },
65
+ });
66
+
67
+ // catalog_view_drop
68
+ catalogUnary(protocol, signingKey, "catalog_view_drop", {
69
+ params: schemaNameIgnoreNotFoundTxnParams,
70
+ result: emptyResultSchema,
71
+ handler: async (params) => {
72
+ const cat = getCatalog();
73
+ await cat.viewDrop(
74
+ toUint8Array(params.attach_opaque_data),
75
+ params.schema_name,
76
+ params.name,
77
+ params.ignore_not_found,
78
+ params.transaction_opaque_data ? toUint8Array(params.transaction_opaque_data) : undefined
79
+ );
80
+ return {};
81
+ },
82
+ });
83
+
84
+ // catalog_view_rename
85
+ catalogUnary(protocol, signingKey, "catalog_view_rename", {
86
+ params: schemaNameRenameParams,
87
+ result: emptyResultSchema,
88
+ handler: async (params) => {
89
+ const cat = getCatalog();
90
+ await cat.viewRename(
91
+ toUint8Array(params.attach_opaque_data),
92
+ params.schema_name,
93
+ params.name,
94
+ params.new_name,
95
+ params.ignore_not_found,
96
+ params.transaction_opaque_data ? toUint8Array(params.transaction_opaque_data) : undefined
97
+ );
98
+ return {};
99
+ },
100
+ });
101
+
102
+ // catalog_view_comment_set
103
+ catalogUnary(protocol, signingKey, "catalog_view_comment_set", {
104
+ params: schemaNameCommentParams,
105
+ result: emptyResultSchema,
106
+ handler: async (params) => {
107
+ const cat = getCatalog();
108
+ await cat.viewCommentSet(
109
+ toUint8Array(params.attach_opaque_data),
110
+ params.schema_name,
111
+ params.name,
112
+ params.comment,
113
+ params.ignore_not_found,
114
+ params.transaction_opaque_data ? toUint8Array(params.transaction_opaque_data) : undefined
115
+ );
116
+ return {};
117
+ },
118
+ });
119
+ }
@@ -0,0 +1,302 @@
1
+ // Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+ // Handlers for the per-function lifecycle: bind, init (exchange), cardinality,
3
+ // statistics. Generic over scalar/table/table-in-out — each function kind
4
+ // implements VgiFunction.bind/globalInit/createStreamHandlers.
5
+
6
+ import { type VgiSchema, schema, type VgiField, field, type VgiDataType, binary, int64 } from "../../arrow/index.js";
7
+ import { Protocol } from "@query-farm/vgi-rpc";
8
+ import type { FunctionRegistry } from "../../functions/registry.js";
9
+ import type { StreamHandlers, HandlerState } from "../../functions/types.js";
10
+ import {
11
+ deserializeBindRequest,
12
+ serializeBindResponse,
13
+ deserializeInitRequest,
14
+ serializeGlobalInitResponse,
15
+ deserializeCardinalityRequest,
16
+ serializeTableCardinality,
17
+ } from "../serialize.js";
18
+ import type { GlobalInitResponse } from "../types.js";
19
+ import { batchToScalarDict, deserializeBatch } from "../../util/arrow/index.js";
20
+ import { toUint8Array } from "../../util/bytes.js";
21
+ import { serializeColumnStatistics } from "../../util/statistics.js";
22
+ import { BindResultSchema, TableFunctionCardinalityResultSchema, TableFunctionDynamicToStringResultSchema } from "../../generated/vgi-protocol-schemas.js";
23
+ import {
24
+ REQUEST_PARAMS_SCHEMA,
25
+ RESULT_BINARY_SCHEMA,
26
+ RESULT_BINARY_NULLABLE_SCHEMA,
27
+ unwrapRequest,
28
+ wrapResult,
29
+ overloadContext,
30
+ recoverFinalizeState,
31
+ } from "./shared.js";
32
+ import { openAttach } from "./catalog/shared.js";
33
+
34
+ export interface FunctionHandlerConfig {
35
+ registry: FunctionRegistry;
36
+ recoverExchangeState?: (opaqueData: Uint8Array) => any;
37
+ signingKey?: Uint8Array;
38
+ }
39
+
40
+ export function registerFunctionMethods(protocol: Protocol, config: FunctionHandlerConfig): void {
41
+ const { registry, signingKey } = config;
42
+
43
+ // The framework mints every attach as uuid(16) || catalog_bytes (sealed on
44
+ // HTTP, plaintext on subprocess). Function bodies — like catalog bodies —
45
+ // must see the catalog's own bytes, so unseal (when keyed) and strip the
46
+ // framework UUID prefix before the user function reads attach_opaque_data.
47
+ async function stripAttach(attach: any, ctx: any): Promise<Uint8Array | null> {
48
+ if (attach == null) return null;
49
+ const env = toUint8Array(attach);
50
+ if (env.length === 0) return env;
51
+ return openAttach(env, ctx?.auth, signingKey);
52
+ }
53
+
54
+ const initHeaderSchema = schema([
55
+ field("execution_id", binary(), false),
56
+ field("opaque_data", binary(), true),
57
+ field("max_workers", int64(), false),
58
+ ]);
59
+
60
+ const emptySchema = schema([]);
61
+
62
+ // Dummy non-empty schema for the init exchange registration.
63
+ // The dispatch determines producer vs exchange based on inputSchema emptiness.
64
+ // We need exchange mode so that input batches are passed to our callback.
65
+ // The actual input schema comes from the IPC stream, not this registration.
66
+ const dummyInputSchema = schema([
67
+ field("_tick", binary(), true),
68
+ ]);
69
+
70
+ // --------------------------------------------------------------------------
71
+ // bind (unary)
72
+ // --------------------------------------------------------------------------
73
+ protocol.unary("bind", {
74
+ params: REQUEST_PARAMS_SCHEMA,
75
+ result: RESULT_BINARY_SCHEMA,
76
+ handler: async (params, ctx) => {
77
+ const innerParams = unwrapRequest(params.request);
78
+ const request = deserializeBindRequest(innerParams);
79
+ request.attach_opaque_data = await stripAttach(request.attach_opaque_data, ctx);
80
+ const func = registry.get(request.function_name, overloadContext(request));
81
+ const response = await func.bind(request);
82
+ const serialized = serializeBindResponse(response);
83
+ return wrapResult(serialized, BindResultSchema);
84
+ },
85
+ });
86
+
87
+ // --------------------------------------------------------------------------
88
+ // init (streaming) - dynamically produces either producer or exchange streams
89
+ // --------------------------------------------------------------------------
90
+ protocol.exchange("init", {
91
+ params: REQUEST_PARAMS_SCHEMA,
92
+ inputSchema: dummyInputSchema,
93
+ outputSchema: emptySchema,
94
+ init: async (params) => {
95
+ // Preserve raw request IPC bytes for exchange reconstruction
96
+ const requestIpcBytes = toUint8Array(params.request);
97
+ const innerParams = unwrapRequest(params.request);
98
+ const request = deserializeInitRequest(innerParams);
99
+ // Exchange init has no ctx; on subprocess (no signing key) openAttach is a
100
+ // pass-through strip that needs no auth. HTTP carries auth via the bind path.
101
+ request.bind_call.attach_opaque_data = await stripAttach(request.bind_call.attach_opaque_data, undefined);
102
+ const func = registry.get(request.bind_call.function_name, overloadContext(request.bind_call));
103
+
104
+ // globalInit is async — table function onInit may touch HTTP-backed
105
+ // FunctionStorage (e.g. Cloudflare DO).
106
+ const initResponse = await func.globalInit(request);
107
+
108
+ // For FINALIZE over HTTP, recover accumulated INPUT state from init_opaque_data.
109
+ // The C++ extension passes the last exchange state token as init_opaque_data.
110
+ const accumulatedState = recoverFinalizeState(request, config.recoverExchangeState);
111
+
112
+ const handlers = func.createStreamHandlers(request, initResponse, accumulatedState);
113
+
114
+ // Initialize the appropriate handler
115
+ let handlerState: HandlerState | undefined;
116
+ if (handlers.producerInit) {
117
+ handlerState = handlers.producerInit();
118
+ } else if (handlers.exchangeInit) {
119
+ handlerState = handlers.exchangeInit();
120
+ }
121
+
122
+ const isProducer = !!handlers.producerFn;
123
+
124
+ // Extract mutable user state from handler (e.g. { remaining, currentIndex })
125
+ // for serialization across HTTP exchanges. The processParams/infrastructure
126
+ // is reconstructed fresh; only user state needs to persist.
127
+ const userState = handlerState?.state ?? null;
128
+
129
+ // Build state object with raw binary data (no base64/hex encoding).
130
+ // The Arrow state serializer picks fields by name; live objects
131
+ // (_handlers, _handlerState, _initResponse, __outputSchema) are ignored.
132
+ const state: any = {
133
+ functionName: request.bind_call.function_name,
134
+ initRequestIpc: requestIpcBytes,
135
+ executionId: initResponse.execution_id,
136
+ maxWorkers: Number(initResponse.max_workers),
137
+ opaqueData: initResponse.opaque_data ?? null,
138
+ isProducer,
139
+ userState,
140
+ __isProducer: isProducer,
141
+ // Live Schemas for vgi-rpc to read during init (not serializable).
142
+ // __inputSchema overrides dispatchStream's method.inputSchema per call
143
+ // — the TS worker registers `init` as exchange with the permissive
144
+ // `dummyInputSchema` sentinel; the real per-function input shape comes
145
+ // from the bound handlers here.
146
+ __outputSchema: handlers.outputSchema ?? emptySchema,
147
+ __inputSchema: handlers.inputSchema ?? emptySchema,
148
+ // Live objects for immediate use during init (producer mode).
149
+ _handlers: handlers,
150
+ _handlerState: handlerState,
151
+ _initResponse: initResponse,
152
+ };
153
+ return state;
154
+ },
155
+ exchange: async (state, input, out) => {
156
+ // Reconstruct live objects from serializable state.
157
+ // This handles both immediate use (producer during init, where _handlers
158
+ // is still present) and deserialized exchange (where we reconstruct).
159
+ let handlers: StreamHandlers;
160
+ let handlerState: HandlerState | undefined;
161
+
162
+ if (state._handlers) {
163
+ // Immediate use (same request, state still in memory)
164
+ handlers = state._handlers;
165
+ handlerState = state._handlerState;
166
+ } else {
167
+ // Deserialized from token — reconstruct from serializable refs.
168
+ // Infrastructure (processParams, BoundStorage) is recreated fresh.
169
+ // Mutable user state is merged from state.userState.
170
+ const initRequestBatch = deserializeBatch(state.initRequestIpc);
171
+ const initRequestDict = batchToScalarDict(initRequestBatch);
172
+ const request = deserializeInitRequest(initRequestDict);
173
+ const func = registry.get(state.functionName, overloadContext(request.bind_call));
174
+ const executionId = state.executionId;
175
+ const opaqueData = state.opaqueData ?? null;
176
+ const initResponse: GlobalInitResponse = {
177
+ execution_id: executionId,
178
+ max_workers: Number(state.maxWorkers ?? 1),
179
+ opaque_data: opaqueData,
180
+ };
181
+
182
+ // Recover accumulated state for FINALIZE phase from initOpaqueData
183
+ const recoveredState = recoverFinalizeState(request, config.recoverExchangeState);
184
+
185
+ handlers = func.createStreamHandlers(request, initResponse, recoveredState);
186
+ if (handlers.producerInit) {
187
+ handlerState = handlers.producerInit();
188
+ } else if (handlers.exchangeInit) {
189
+ handlerState = handlers.exchangeInit();
190
+ }
191
+ // Merge preserved user state (e.g. { remaining, currentIndex })
192
+ if (state.userState != null && handlerState?.state !== undefined) {
193
+ handlerState!.state = state.userState;
194
+ }
195
+ }
196
+
197
+ if (state.isProducer && handlers.producerFn) {
198
+ // Producer mode inside an exchange dispatch: patch finish() to bypass
199
+ // OutputCollector's producerMode check (since dispatch created it
200
+ // in exchange mode but we're actually producing).
201
+ out.finish = () => { (out as any)._finished = true; };
202
+ // Let the handler observe tick-batch metadata before producing. Used
203
+ // by table functions to apply dynamic-filter updates DuckDB attaches
204
+ // as `vgi_pushdown_filters` on each tick (Top-N heap tightening).
205
+ if (handlers.onTick) {
206
+ await handlers.onTick(handlerState, (input as any)?.metadata);
207
+ }
208
+ await handlers.producerFn(handlerState, out);
209
+ } else if (handlers.exchangeFn) {
210
+ await handlers.exchangeFn(handlerState, input, out);
211
+ }
212
+
213
+ // Save mutated user state for the next exchange round
214
+ if (handlerState?.state !== undefined) {
215
+ state.userState = handlerState!.state;
216
+ }
217
+ },
218
+ headerSchema: initHeaderSchema,
219
+ headerInit: (params: any, state: any, ctx: any) => {
220
+ // During init, _initResponse is still in memory.
221
+ // For exchange, this is never called (headers are only in init response).
222
+ if (!state._initResponse) {
223
+ throw new Error("headerInit called on deserialized state: _initResponse not available");
224
+ }
225
+ return serializeGlobalInitResponse(state._initResponse);
226
+ },
227
+ });
228
+
229
+ // --------------------------------------------------------------------------
230
+ // table_function_cardinality (unary)
231
+ // --------------------------------------------------------------------------
232
+ protocol.unary("table_function_cardinality", {
233
+ params: REQUEST_PARAMS_SCHEMA,
234
+ result: RESULT_BINARY_SCHEMA,
235
+ handler: async (params) => {
236
+ const innerParams = unwrapRequest(params.request);
237
+ const request = deserializeCardinalityRequest(innerParams);
238
+ const func = registry.get(request.bind_call.function_name, overloadContext(request.bind_call));
239
+ let cardResult: Record<string, any>;
240
+ if (func.cardinality) {
241
+ cardResult = serializeTableCardinality(await func.cardinality(request));
242
+ } else {
243
+ cardResult = { estimate: null, max: null };
244
+ }
245
+ return wrapResult(cardResult, TableFunctionCardinalityResultSchema);
246
+ },
247
+ });
248
+
249
+ // --------------------------------------------------------------------------
250
+ // table_function_statistics (unary)
251
+ // --------------------------------------------------------------------------
252
+ // Returns bytes-or-null result: serialized ColumnStatistics RecordBatch when
253
+ // the function declared a statistics() hook and it produced a non-empty
254
+ // list, else null. DuckDB uses the bounds for plan-time filter elimination
255
+ // (folds impossible filters to EMPTY_RESULT).
256
+ protocol.unary("table_function_statistics", {
257
+ params: REQUEST_PARAMS_SCHEMA,
258
+ result: RESULT_BINARY_NULLABLE_SCHEMA,
259
+ handler: (params) => {
260
+ const innerParams = unwrapRequest(params.request);
261
+ const request = deserializeCardinalityRequest(innerParams);
262
+ const func = registry.get(request.bind_call.function_name, overloadContext(request.bind_call));
263
+ if (!func.statistics) return { result: null };
264
+ const stats = func.statistics(request);
265
+ if (!stats || stats.length === 0) return { result: null };
266
+ return { result: serializeColumnStatistics(stats) };
267
+ },
268
+ });
269
+
270
+ // --------------------------------------------------------------------------
271
+ // table_function_dynamic_to_string (unary)
272
+ // --------------------------------------------------------------------------
273
+ // DuckDB calls this once per parallel scan thread at FinishSource. The
274
+ // result is a List<Utf8>/List<Utf8> pair carrying ordered key→value
275
+ // diagnostics that surface under EXPLAIN ANALYZE alongside the
276
+ // intrinsics (Function, Rows Read, Threads). When the function doesn't
277
+ // declare dynamicToString, return empty maps so the C++ side falls back
278
+ // to intrinsics only.
279
+ protocol.unary("table_function_dynamic_to_string", {
280
+ params: REQUEST_PARAMS_SCHEMA,
281
+ result: RESULT_BINARY_SCHEMA,
282
+ handler: async (params) => {
283
+ const innerParams = unwrapRequest(params.request);
284
+ const bindCallBytes = toUint8Array(innerParams.bind_call);
285
+ const bindCallBatch = deserializeBatch(bindCallBytes);
286
+ // Single-row bind_call batch -> dict via the codec/canonical path.
287
+ const bindParams = batchToScalarDict(bindCallBatch);
288
+ const bindCall = deserializeBindRequest(bindParams);
289
+ const globalExecutionId = toUint8Array(innerParams.global_execution_id);
290
+ const bindOpaqueData = innerParams.bind_opaque_data
291
+ ? toUint8Array(innerParams.bind_opaque_data)
292
+ : null;
293
+ const func = registry.get(bindCall.function_name, overloadContext(bindCall));
294
+ const map = func.dynamicToString
295
+ ? await func.dynamicToString({ bindCall, bindOpaqueData, globalExecutionId })
296
+ : {};
297
+ const keys = Object.keys(map);
298
+ const values = keys.map((k) => map[k] ?? "");
299
+ return wrapResult({ keys, values }, TableFunctionDynamicToStringResultSchema);
300
+ },
301
+ });
302
+ }
@@ -0,0 +1,80 @@
1
+ // Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+ // Shared schemas and helpers used by every protocol handler.
3
+
4
+ import { type VgiSchema, schema, type VgiField, field, type VgiDataType, binary } from "../../arrow/index.js";
5
+ import type { OverloadContext } from "../../functions/registry.js";
6
+ import { batchToScalarDict, deserializeBatch, serializeBatch, batchFromColumns } from "../../util/arrow/index.js";
7
+ import { toUint8Array } from "../../util/bytes.js";
8
+ import { TableInOutPhase } from "../../types.js";
9
+ import type { InitRequest } from "../types.js";
10
+
11
+ // The Python vgi-rpc framework wraps ALL non-void unary results in a single
12
+ // "result" column. For ArrowSerializableDataclass types, the result is serialized
13
+ // as Arrow IPC bytes in a Binary column. DuckDB's VGI extension expects this format.
14
+ export const RESULT_BINARY_SCHEMA = schema([
15
+ field("result", binary(), false),
16
+ ]);
17
+
18
+ // DuckDB wraps ArrowSerializableDataclass parameters in a single "request" Binary column.
19
+ export const REQUEST_PARAMS_SCHEMA = schema([
20
+ field("request", binary(), false),
21
+ ]);
22
+
23
+ export const RESULT_BINARY_NULLABLE_SCHEMA = schema([
24
+ field("result", binary(), true),
25
+ ]);
26
+
27
+ /**
28
+ * Unwrap a "request" Binary column: deserialize the inner Arrow IPC batch
29
+ * and return flat columns as a dict (row 0).
30
+ */
31
+ export function unwrapRequest(requestBytes: any): Record<string, any> {
32
+ const bytes = toUint8Array(requestBytes);
33
+ const innerBatch = deserializeBatch(bytes);
34
+ return batchToScalarDict(innerBatch);
35
+ }
36
+
37
+ /**
38
+ * Wrap a dict of values into a single "result" Binary column.
39
+ * Builds a 1-row batch from the values using the given schema,
40
+ * serializes it to IPC bytes, and returns { result: bytes }.
41
+ */
42
+ export function wrapResult(
43
+ values: Record<string, any>,
44
+ innerSchema: VgiSchema | import("../../arrow/index.js").VgiSchema,
45
+ ): { result: Uint8Array } {
46
+ const a = innerSchema as VgiSchema;
47
+ const batch = batchFromColumns(
48
+ Object.fromEntries(a.fields.map(f => [f.name, [values[f.name] ?? null]])),
49
+ a,
50
+ );
51
+ return { result: serializeBatch(batch) };
52
+ }
53
+
54
+ export function overloadContext(req: { function_name: string; arguments: any; input_schema: any; function_type: any }): OverloadContext {
55
+ return {
56
+ arguments: req.arguments,
57
+ inputSchema: req.input_schema,
58
+ isScalar: String(req.function_type).toLowerCase() === "scalar",
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Recover accumulated exchange state from a FINALIZE init request.
64
+ * For HTTP transport, this unpacks the state token that the C++ extension
65
+ * passes from the last INPUT exchange to the FINALIZE init request.
66
+ */
67
+ export function recoverFinalizeState(
68
+ request: InitRequest,
69
+ recoverExchangeState: ((opaqueData: Uint8Array) => any) | undefined,
70
+ ): any {
71
+ if (request.phase === TableInOutPhase.FINALIZE && request.init_opaque_data && recoverExchangeState) {
72
+ try {
73
+ const recovered = recoverExchangeState(request.init_opaque_data);
74
+ return recovered?.userState;
75
+ } catch (e: any) {
76
+ throw new Error(`Failed to recover FINALIZE state from init_opaque_data: ${e.message}`);
77
+ }
78
+ }
79
+ return undefined;
80
+ }