@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,52 @@
1
+ // Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+ // Build VGI protocol from worker implementation.
3
+ // Orchestrator that registers function, aggregate, and catalog handlers
4
+ // against vgi-rpc Protocol. The handler implementations live in handlers/.
5
+
6
+ import { Protocol } from "@query-farm/vgi-rpc";
7
+
8
+ // NOTE: We must NOT use vgi-rpc's str/bytes/int/etc. singletons in Schema objects
9
+ // because Bun loads apache-arrow as separate module instances for our code vs vgi-rpc's
10
+ // compiled dist. Instead, we pre-build Schema objects and pass them directly to Protocol
11
+ // methods (toSchema() passes Schema instances through without instanceof checks).
12
+ import type { FunctionRegistry } from "../functions/registry.js";
13
+ import type { CatalogInterface } from "../catalog/interface.js";
14
+ import { registerFunctionMethods } from "./handlers/function.js";
15
+ import { registerAggregateMethods } from "./handlers/aggregate.js";
16
+ import { registerTableBufferingMethods } from "./handlers/table-buffering.js";
17
+ import { registerCatalogMethods } from "./handlers/catalog/index.js";
18
+
19
+ export interface ProtocolConfig {
20
+ registry: FunctionRegistry;
21
+ catalogInterface?: CatalogInterface;
22
+ catalogName?: string;
23
+ /**
24
+ * Recover accumulated exchange state from FINALIZE init_opaque_data.
25
+ * For HTTP transport, this unpacks the state token that the C++ extension
26
+ * passes from the last INPUT exchange to the FINALIZE init request.
27
+ * Returns the deserialized VGI dispatch state object (with userState field).
28
+ */
29
+ recoverExchangeState?: (opaqueData: Uint8Array) => any;
30
+ /**
31
+ * AEAD signing key for sealing catalog opaque-data envelopes. Pass the same
32
+ * 32-byte key used for HTTP state tokens. When omitted (subprocess / unix
33
+ * transports) attach_opaque_data / transaction_opaque_data pass through
34
+ * unsealed — OS process ownership already enforces identity there.
35
+ */
36
+ signingKey?: Uint8Array;
37
+ }
38
+
39
+ export function buildVgiProtocol(config: ProtocolConfig): Protocol {
40
+ const protocol = new Protocol("vgi");
41
+
42
+ registerFunctionMethods(protocol, {
43
+ registry: config.registry,
44
+ recoverExchangeState: config.recoverExchangeState,
45
+ signingKey: config.signingKey,
46
+ });
47
+ registerAggregateMethods(protocol, config.registry);
48
+ registerTableBufferingMethods(protocol, config.registry, config.signingKey);
49
+ registerCatalogMethods(protocol, config.catalogInterface, config.catalogName, config.signingKey);
50
+
51
+ return protocol;
52
+ }
@@ -0,0 +1,355 @@
1
+ // Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+ // Handlers for the aggregate function lifecycle: bind allocates execution_id
3
+ // and output schema, update folds rows into per-group state, combine merges
4
+ // states across workers, finalize emits one result row per group, destructor
5
+ // drops finished group state.
6
+
7
+ import {
8
+ type VgiSchema,
9
+ type VgiBatch,
10
+ type VgiDataType,
11
+ schema as makeSchema,
12
+ field,
13
+ isDecimal,
14
+ serializeBatch,
15
+ deserializeBatch,
16
+ } from "../../arrow/index.js";
17
+ import { Protocol } from "@query-farm/vgi-rpc";
18
+ import type { FunctionRegistry } from "../../functions/registry.js";
19
+ import { deserializeArguments } from "../serialize.js";
20
+ import { deserializeSchema, serializeSchema, batchToSecretDict } from "../../util/arrow/index.js";
21
+ import { toUint8Array } from "../../util/bytes.js";
22
+ import {
23
+ AggregateBindResultSchema,
24
+ AggregateFinalizeResultSchema,
25
+ } from "../../generated/vgi-protocol-schemas.js";
26
+ import {
27
+ getExecutionState,
28
+ setExecutionState,
29
+ persistGroupStates,
30
+ GROUP_COLUMN_NAME,
31
+ type AggregateFunctionConfig,
32
+ type AggregateBindParams,
33
+ } from "../../functions/aggregate.js";
34
+ import { Arguments } from "../../arguments/arguments.js";
35
+ import {
36
+ REQUEST_PARAMS_SCHEMA,
37
+ RESULT_BINARY_SCHEMA,
38
+ unwrapRequest,
39
+ wrapResult,
40
+ } from "./shared.js";
41
+
42
+ export function registerAggregateMethods(protocol: Protocol, registry: FunctionRegistry): void {
43
+ function resolveAggregate(name: string): AggregateFunctionConfig<any, any> {
44
+ const func = registry.get(name, { arguments: new Arguments(), inputSchema: null, isScalar: false }) as any;
45
+ if (!func || func.kind !== "aggregate") {
46
+ throw new Error(`Function '${name}' is not an aggregate function`);
47
+ }
48
+ return func.aggregateConfig as AggregateFunctionConfig<any, any>;
49
+ }
50
+
51
+ // ------------------------------------------------------------------------
52
+ // aggregate_bind — allocate execution_id, return output schema
53
+ // ------------------------------------------------------------------------
54
+ protocol.unary("aggregate_bind", {
55
+ params: REQUEST_PARAMS_SCHEMA,
56
+ result: RESULT_BINARY_SCHEMA,
57
+ handler: async (params) => {
58
+ const innerParams = unwrapRequest(params.request);
59
+ const functionName: string = innerParams.function_name;
60
+ const cfg = resolveAggregate(functionName);
61
+ const argBytes = toUint8Array(innerParams.arguments);
62
+ const args = deserializeArguments(argBytes);
63
+ const inputSchema = innerParams.input_schema
64
+ ? deserializeSchema(toUint8Array(innerParams.input_schema))
65
+ : null;
66
+ // Decode the resolved-secrets batch the extension pre-resolves for
67
+ // aggregates that declared `requiredSecrets`. Mirrors the table-buffering
68
+ // handler (batchToSecretDict). The secret VALUES are delivered only here,
69
+ // at bind time — update/combine/finalize see no secrets by design.
70
+ const secretsBytes = innerParams.secrets ? toUint8Array(innerParams.secrets) : null;
71
+ const secretsBatch =
72
+ secretsBytes && secretsBytes.length > 0 ? deserializeBatch(secretsBytes) : null;
73
+ const secrets = secretsBatch ? batchToSecretDict(secretsBatch) : {};
74
+ const bindParams: AggregateBindParams<any> = {
75
+ args: extractArgMap(cfg, args),
76
+ arguments: args,
77
+ inputSchema,
78
+ settings: {},
79
+ secrets,
80
+ };
81
+ const executionId = new Uint8Array(16);
82
+ crypto.getRandomValues(executionId);
83
+ setExecutionState(executionId, {
84
+ states: new Map(),
85
+ bindArgs: args,
86
+ bindParams,
87
+ });
88
+ // Output schema is a single "result" column. Type comes from the
89
+ // optional onBind hook (for aggregates whose return type depends on
90
+ // input — e.g. vgi_generic_sum takes ANY and returns the same type);
91
+ // otherwise the declared cfg.outputType.
92
+ const outType = cfg.onBind ? await cfg.onBind(bindParams) : cfg.outputType;
93
+ const outSchema = makeSchema([field("result", outType, true)]);
94
+ return wrapResult(
95
+ { output_schema: serializeSchema(outSchema), execution_id: executionId },
96
+ AggregateBindResultSchema,
97
+ );
98
+ },
99
+ });
100
+
101
+ // ------------------------------------------------------------------------
102
+ // aggregate_update — fold the incoming batch into per-group state
103
+ // ------------------------------------------------------------------------
104
+ protocol.unary("aggregate_update", {
105
+ params: REQUEST_PARAMS_SCHEMA,
106
+ result: RESULT_BINARY_SCHEMA,
107
+ handler: (params) => {
108
+ const innerParams = unwrapRequest(params.request);
109
+ const functionName: string = innerParams.function_name;
110
+ const executionId = toUint8Array(innerParams.execution_id);
111
+ const cfg = resolveAggregate(functionName);
112
+ const batch = readSingleBatch(toUint8Array(innerParams.input_batch));
113
+ if (!batch || batch.numRows === 0) return wrapResult({}, makeSchema([]));
114
+
115
+ const gidIdx = batch.schema.fields.findIndex((f) => f.name === GROUP_COLUMN_NAME);
116
+ if (gidIdx < 0) {
117
+ throw new Error(`aggregate_update: input batch missing ${GROUP_COLUMN_NAME} column`);
118
+ }
119
+ const gidCol = batch.getChildAt(gidIdx)!;
120
+ const groupIds: bigint[] = [];
121
+ const uniqueGids = new Set<bigint>();
122
+ for (let i = 0; i < batch.numRows; i++) {
123
+ const v = gidCol.get(i);
124
+ const g = typeof v === "bigint" ? v : BigInt(v as any);
125
+ groupIds.push(g);
126
+ uniqueGids.add(g);
127
+ }
128
+ const exec = getExecutionState(executionId, uniqueGids);
129
+ if (!exec) {
130
+ throw new Error(`aggregate_update: unknown execution_id for '${functionName}' (bind missing or state dropped)`);
131
+ }
132
+
133
+ // Build column-arg arrays — every non-__vgi_group_id column, in
134
+ // schema order — and pass to user's update(). We deliberately do NOT
135
+ // pre-allocate state for every group_id seen — the user decides when
136
+ // to call ensureState() so that groups with only-NULL input (skipped
137
+ // rows) never get an entry, which lets finalize() return SQL NULL.
138
+ const columns: any[] = [];
139
+ for (let i = 0; i < batch.schema.fields.length; i++) {
140
+ if (i === gidIdx) continue;
141
+ columns.push(batch.getChildAt(i));
142
+ }
143
+ const ensureState = (gid: bigint) => {
144
+ let s = exec.states.get(gid);
145
+ if (s === undefined) {
146
+ s = cfg.initialState(exec.bindParams);
147
+ exec.states.set(gid, s);
148
+ }
149
+ return s;
150
+ };
151
+ cfg.update({
152
+ states: exec.states,
153
+ groupIds,
154
+ columns,
155
+ args: exec.bindParams.args,
156
+ ensureState,
157
+ });
158
+ // Flush mutated states to disk so sibling workers see them. We persist
159
+ // every group that was referenced this call — some may be unchanged
160
+ // (skipped by NULL handling), in which case the file simply reflects
161
+ // the pre-existing initial state; the idempotent rewrite is cheap
162
+ // relative to the RPC overhead.
163
+ persistGroupStates(executionId, uniqueGids);
164
+ return wrapResult({}, makeSchema([]));
165
+ },
166
+ });
167
+
168
+ // ------------------------------------------------------------------------
169
+ // aggregate_combine — merge (source_group_id, target_group_id) pairs
170
+ // ------------------------------------------------------------------------
171
+ protocol.unary("aggregate_combine", {
172
+ params: REQUEST_PARAMS_SCHEMA,
173
+ result: RESULT_BINARY_SCHEMA,
174
+ handler: (params) => {
175
+ const innerParams = unwrapRequest(params.request);
176
+ const functionName: string = innerParams.function_name;
177
+ const executionId = toUint8Array(innerParams.execution_id);
178
+ const cfg = resolveAggregate(functionName);
179
+ const batch = readSingleBatch(toUint8Array(innerParams.merge_batch));
180
+ if (!batch || batch.numRows === 0) return wrapResult({}, makeSchema([]));
181
+
182
+ const srcCol = batch.getChild("source_group_id")!;
183
+ const tgtCol = batch.getChild("target_group_id")!;
184
+ const referencedGids = new Set<bigint>();
185
+ const srcIds: bigint[] = [];
186
+ const tgtIds: bigint[] = [];
187
+ for (let i = 0; i < batch.numRows; i++) {
188
+ const src = BigInt(srcCol.get(i) as any);
189
+ const tgt = BigInt(tgtCol.get(i) as any);
190
+ srcIds.push(src); tgtIds.push(tgt);
191
+ referencedGids.add(src); referencedGids.add(tgt);
192
+ }
193
+ const exec = getExecutionState(executionId, referencedGids);
194
+ if (!exec) {
195
+ throw new Error(
196
+ `aggregate_combine: unknown execution_id for '${functionName}' ` +
197
+ `(context missing on disk — bind may have failed or been cleaned up)`,
198
+ );
199
+ }
200
+ const touchedTargets = new Set<bigint>();
201
+ for (let i = 0; i < batch.numRows; i++) {
202
+ const src = srcIds[i];
203
+ const tgt = tgtIds[i];
204
+ const srcHas = exec.states.has(src);
205
+ const tgtHas = exec.states.has(tgt);
206
+ if (!srcHas && !tgtHas) continue;
207
+ const srcState = srcHas ? exec.states.get(src)! : cfg.initialState(exec.bindParams);
208
+ const tgtState = tgtHas ? exec.states.get(tgt)! : cfg.initialState(exec.bindParams);
209
+ exec.states.set(tgt, cfg.combine(srcState, tgtState, exec.bindParams));
210
+ touchedTargets.add(tgt);
211
+ }
212
+ persistGroupStates(executionId, touchedTargets);
213
+ return wrapResult({}, makeSchema([]));
214
+ },
215
+ });
216
+
217
+ // ------------------------------------------------------------------------
218
+ // aggregate_finalize — emit one result row per requested group_id
219
+ // ------------------------------------------------------------------------
220
+ protocol.unary("aggregate_finalize", {
221
+ params: REQUEST_PARAMS_SCHEMA,
222
+ result: RESULT_BINARY_SCHEMA,
223
+ handler: (params) => {
224
+ const innerParams = unwrapRequest(params.request);
225
+ const functionName: string = innerParams.function_name;
226
+ const executionId = toUint8Array(innerParams.execution_id);
227
+ const cfg = resolveAggregate(functionName);
228
+ const outputSchema = deserializeSchema(toUint8Array(innerParams.output_schema));
229
+ const gidBatch = readSingleBatch(toUint8Array(innerParams.group_ids_batch));
230
+ const groupIds: bigint[] = [];
231
+ const uniqueGids = new Set<bigint>();
232
+ if (gidBatch) {
233
+ const col = gidBatch.getChild("group_id")!;
234
+ for (let i = 0; i < gidBatch.numRows; i++) {
235
+ const v = col.get(i);
236
+ const g = typeof v === "bigint" ? v : BigInt(v as any);
237
+ groupIds.push(g);
238
+ uniqueGids.add(g);
239
+ }
240
+ }
241
+ const exec = getExecutionState(executionId, uniqueGids);
242
+ if (!exec) {
243
+ throw new Error(`aggregate_finalize: unknown execution_id for '${functionName}'`);
244
+ }
245
+ // Groups never updated pass state = null so finalize() can emit SQL NULL
246
+ // (matches Python semantics for SUM/AVG/MIN/MAX over zero rows).
247
+ const states = new Map<bigint, any | null>();
248
+ for (const gid of groupIds) {
249
+ states.set(gid, exec.states.has(gid) ? exec.states.get(gid) : null);
250
+ }
251
+ const resultBatch = cfg.finalize({
252
+ groupIds, states, outputSchema, args: exec.bindParams.args,
253
+ });
254
+ return wrapResult(
255
+ { result_batch: serializeBatchWithSchema(resultBatch) },
256
+ AggregateFinalizeResultSchema,
257
+ );
258
+ },
259
+ });
260
+
261
+ // ------------------------------------------------------------------------
262
+ // aggregate_destructor — drop state for finished groups (best-effort)
263
+ //
264
+ // DuckDB sends a batch of group_ids to discard; we remove each from the
265
+ // in-memory state map. We DO NOT tear down the entire execution on an
266
+ // empty batch — DuckDB may still call finalize afterwards in some plans
267
+ // (matches Python's conservative behavior; the execution entry is cleaned
268
+ // up when the worker process exits).
269
+ // ------------------------------------------------------------------------
270
+ protocol.unary("aggregate_destructor", {
271
+ params: REQUEST_PARAMS_SCHEMA,
272
+ result: RESULT_BINARY_SCHEMA,
273
+ handler: (params) => {
274
+ const innerParams = unwrapRequest(params.request);
275
+ const executionId = toUint8Array(innerParams.execution_id);
276
+ const exec = getExecutionState(executionId);
277
+ if (!exec) return wrapResult({}, makeSchema([]));
278
+ const gidBatch = readSingleBatch(toUint8Array(innerParams.group_ids_batch));
279
+ if (!gidBatch || gidBatch.numRows === 0) return wrapResult({}, makeSchema([]));
280
+ const col = gidBatch.getChild("group_id")!;
281
+ const toDrop = new Set<bigint>();
282
+ for (let i = 0; i < gidBatch.numRows; i++) {
283
+ const v = col.get(i);
284
+ const g = typeof v === "bigint" ? v : BigInt(v as any);
285
+ exec.states.delete(g);
286
+ toDrop.add(g);
287
+ }
288
+ // Persist deletions (unlinks the per-group file so sibling workers
289
+ // don't revive the state via getExecutionState's lazy disk load).
290
+ persistGroupStates(executionId, toDrop);
291
+ return wrapResult({}, makeSchema([]));
292
+ },
293
+ });
294
+ }
295
+
296
+ function extractArgMap(
297
+ cfg: AggregateFunctionConfig<any, any>,
298
+ args: Arguments,
299
+ ): Record<string, any> {
300
+ const out: Record<string, any> = {};
301
+ // The wire-side Arguments contains only const params — DuckDB erases const
302
+ // args from the expression tree at bind time and serializes their values
303
+ // into the aggregate_bind request's `arguments` field. So we index the
304
+ // Arguments by the declaration order of const params only.
305
+ const constSet = new Set(cfg.constParams ?? []);
306
+ const constNames: string[] = [];
307
+ if (cfg.args) {
308
+ for (const name of Object.keys(cfg.args)) {
309
+ if (constSet.has(name)) constNames.push(name);
310
+ }
311
+ }
312
+ for (let i = 0; i < constNames.length; i++) {
313
+ try {
314
+ let v = args.get(i);
315
+ // Decode DECIMAL scalars (DuckDB serializes 0.5-style literals as
316
+ // DECIMAL(2,1), which Arrow stores as the unscaled integer — a raw
317
+ // BigInt here). Scale back to float using the declared scale.
318
+ const field = args.argumentsSchema?.fields.find(f => f.name === `positional_${i}`);
319
+ if (field && isDecimal(field.type)) {
320
+ const scale = (field.type as any).scale;
321
+ if (typeof v === "bigint") v = Number(v) / Math.pow(10, scale);
322
+ else if (typeof v === "number") v = v / Math.pow(10, scale);
323
+ } else if (typeof v === "bigint") {
324
+ v = Number(v);
325
+ }
326
+ out[constNames[i]] = v;
327
+ } catch {
328
+ // args.get throws when the arg isn't set — fall back to the declared
329
+ // default so aggregates called without an explicit const value still
330
+ // see it in update/finalize.
331
+ const def = cfg.argDefaults?.[constNames[i]];
332
+ if (def !== undefined) out[constNames[i]] = def;
333
+ }
334
+ }
335
+ // Non-const args aren't in the wire Arguments, but users may still want
336
+ // defaults surfaced for `params.args` — apply them here.
337
+ if (cfg.argDefaults) {
338
+ for (const [name, def] of Object.entries(cfg.argDefaults)) {
339
+ if (!(name in out) && !constSet.has(name)) out[name] = def;
340
+ }
341
+ }
342
+ return out;
343
+ }
344
+
345
+ function readSingleBatch(bytes: Uint8Array): VgiBatch | null {
346
+ if (!bytes || bytes.length === 0) return null;
347
+ const batch = deserializeBatch(bytes);
348
+ return batch.numRows === 0 && batch.schema.fields.length === 0 ? null : batch;
349
+ }
350
+
351
+ function serializeBatchWithSchema(batch: VgiBatch): Uint8Array {
352
+ // serializeBatch emits a full IPC stream (schema + batch + EOS) — the C++
353
+ // aggregate_finalize reader expects exactly that.
354
+ return serializeBatch(batch);
355
+ }