@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.
- package/LICENSE +134 -0
- package/README.md +494 -0
- package/dist/__dbg.test-d.d.ts +1 -0
- package/dist/arguments/argument-spec.d.ts +51 -0
- package/dist/arguments/arguments.d.ts +13 -0
- package/dist/arrow/codec/branded.d.ts +83 -0
- package/dist/arrow/codec/registry.d.ts +29 -0
- package/dist/arrow/codec/repr.d.ts +23 -0
- package/dist/arrow/codec/type-descriptors.d.ts +109 -0
- package/dist/arrow/impl-arrowjs/build.d.ts +19 -0
- package/dist/arrow/impl-arrowjs/canonical.d.ts +15 -0
- package/dist/arrow/impl-arrowjs/empty.d.ts +7 -0
- package/dist/arrow/impl-arrowjs/filter.d.ts +11 -0
- package/dist/arrow/impl-arrowjs/index.d.ts +11 -0
- package/dist/arrow/impl-arrowjs/ipc.d.ts +24 -0
- package/dist/arrow/impl-arrowjs/iterate.d.ts +39 -0
- package/dist/arrow/impl-arrowjs/project.d.ts +10 -0
- package/dist/arrow/impl-arrowjs/schema.d.ts +62 -0
- package/dist/arrow/impl-arrowjs/statistics.d.ts +13 -0
- package/dist/arrow/impl-flechette/build.d.ts +14 -0
- package/dist/arrow/impl-flechette/canonical.d.ts +18 -0
- package/dist/arrow/impl-flechette/empty.d.ts +9 -0
- package/dist/arrow/impl-flechette/filter.d.ts +10 -0
- package/dist/arrow/impl-flechette/index.d.ts +11 -0
- package/dist/arrow/impl-flechette/ipc.d.ts +33 -0
- package/dist/arrow/impl-flechette/iterate.d.ts +29 -0
- package/dist/arrow/impl-flechette/message-meta.d.ts +10 -0
- package/dist/arrow/impl-flechette/normalize-type.d.ts +11 -0
- package/dist/arrow/impl-flechette/project.d.ts +3 -0
- package/dist/arrow/impl-flechette/schema.d.ts +47 -0
- package/dist/arrow/impl-flechette/statistics.d.ts +13 -0
- package/dist/arrow/index.d.ts +11 -0
- package/dist/arrow/predicates.d.ts +47 -0
- package/dist/arrow/repr.test-d.d.ts +52 -0
- package/dist/arrow/schema-types.d.ts +71 -0
- package/dist/arrow/types.d.ts +57 -0
- package/dist/catalog/attach-option.d.ts +33 -0
- package/dist/catalog/attach-options.d.ts +30 -0
- package/dist/catalog/composite.d.ts +33 -0
- package/dist/catalog/descriptors.d.ts +154 -0
- package/dist/catalog/interface.d.ts +239 -0
- package/dist/catalog/read-only.d.ts +33 -0
- package/dist/client/client.d.ts +189 -0
- package/dist/client/errors.d.ts +26 -0
- package/dist/client/helpers.d.ts +12 -0
- package/dist/client/protocol.d.ts +16 -0
- package/dist/client/types.d.ts +125 -0
- package/dist/client-entry.d.ts +11 -0
- package/dist/client-entry.js +48 -0
- package/dist/client-entry.js.map +9 -0
- package/dist/codec/asd.d.ts +25 -0
- package/dist/crypto.d.ts +36 -0
- package/dist/errors.d.ts +24 -0
- package/dist/filter-pushdown/collector.d.ts +32 -0
- package/dist/filter-pushdown/deserialize.d.ts +16 -0
- package/dist/filter-pushdown/evaluate.d.ts +51 -0
- package/dist/filter-pushdown/index.d.ts +4 -0
- package/dist/filter-pushdown/types.d.ts +84 -0
- package/dist/functions/aggregate.d.ts +129 -0
- package/dist/functions/copy-from.d.ts +81 -0
- package/dist/functions/copy-to.d.ts +121 -0
- package/dist/functions/registry.d.ts +16 -0
- package/dist/functions/scalar.d.ts +98 -0
- package/dist/functions/storage-cf-do.d.ts +47 -0
- package/dist/functions/storage.d.ts +86 -0
- package/dist/functions/table-buffering.d.ts +99 -0
- package/dist/functions/table-in-out.d.ts +64 -0
- package/dist/functions/table.d.ts +118 -0
- package/dist/functions/types.d.ts +157 -0
- package/dist/generated/vgi-client.d.ts +466 -0
- package/dist/generated/vgi-protocol-schemas.d.ts +115 -0
- package/dist/index.core.d.ts +42 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10888 -0
- package/dist/index.js.map +85 -0
- package/dist/metadata/resolve.d.ts +3 -0
- package/dist/metadata/serialize.d.ts +12 -0
- package/dist/metadata/types.d.ts +47 -0
- package/dist/protocol/dispatch.d.ts +23 -0
- package/dist/protocol/handlers/aggregate.d.ts +3 -0
- package/dist/protocol/handlers/catalog/admin.d.ts +3 -0
- package/dist/protocol/handlers/catalog/index.d.ts +3 -0
- package/dist/protocol/handlers/catalog/index_methods.d.ts +3 -0
- package/dist/protocol/handlers/catalog/macro.d.ts +3 -0
- package/dist/protocol/handlers/catalog/shared.d.ts +70 -0
- package/dist/protocol/handlers/catalog/table.d.ts +3 -0
- package/dist/protocol/handlers/catalog/view.d.ts +3 -0
- package/dist/protocol/handlers/function.d.ts +8 -0
- package/dist/protocol/handlers/shared.d.ts +31 -0
- package/dist/protocol/handlers/table-buffering.d.ts +3 -0
- package/dist/protocol/serialize.d.ts +4 -0
- package/dist/protocol/serializers/arguments.d.ts +3 -0
- package/dist/protocol/serializers/bind.d.ts +9 -0
- package/dist/protocol/serializers/cardinality.d.ts +7 -0
- package/dist/protocol/serializers/init.d.ts +9 -0
- package/dist/protocol/serializers/shared.d.ts +12 -0
- package/dist/protocol/state-serializer.d.ts +23 -0
- package/dist/protocol/types.d.ts +129 -0
- package/dist/secrets/helpers.d.ts +15 -0
- package/dist/types.d.ts +52 -0
- package/dist/util/arrow/index.d.ts +1 -0
- package/dist/util/bytes.d.ts +2 -0
- package/dist/util/statistics.d.ts +7 -0
- package/dist/worker-cf-entry.d.ts +30 -0
- package/dist/worker-cf-entry.js +8767 -0
- package/dist/worker-cf-entry.js.map +74 -0
- package/dist/worker.d.ts +36 -0
- package/package.json +96 -0
- package/src/arguments/argument-spec.ts +200 -0
- package/src/arguments/arguments.ts +81 -0
- package/src/arrow/codec/branded.ts +206 -0
- package/src/arrow/codec/registry.ts +674 -0
- package/src/arrow/codec/repr.ts +181 -0
- package/src/arrow/codec/type-descriptors.ts +137 -0
- package/src/arrow/impl-arrowjs/build.ts +88 -0
- package/src/arrow/impl-arrowjs/canonical.ts +509 -0
- package/src/arrow/impl-arrowjs/empty.ts +72 -0
- package/src/arrow/impl-arrowjs/filter.ts +53 -0
- package/src/arrow/impl-arrowjs/index.ts +54 -0
- package/src/arrow/impl-arrowjs/ipc.ts +67 -0
- package/src/arrow/impl-arrowjs/iterate.ts +159 -0
- package/src/arrow/impl-arrowjs/project.ts +73 -0
- package/src/arrow/impl-arrowjs/schema.ts +256 -0
- package/src/arrow/impl-arrowjs/statistics.ts +185 -0
- package/src/arrow/impl-flechette/build.ts +65 -0
- package/src/arrow/impl-flechette/canonical.ts +289 -0
- package/src/arrow/impl-flechette/empty.ts +20 -0
- package/src/arrow/impl-flechette/filter.ts +42 -0
- package/src/arrow/impl-flechette/index.ts +54 -0
- package/src/arrow/impl-flechette/ipc.ts +110 -0
- package/src/arrow/impl-flechette/iterate.ts +121 -0
- package/src/arrow/impl-flechette/message-meta.ts +175 -0
- package/src/arrow/impl-flechette/normalize-type.ts +99 -0
- package/src/arrow/impl-flechette/project.ts +64 -0
- package/src/arrow/impl-flechette/schema.ts +202 -0
- package/src/arrow/impl-flechette/statistics.ts +198 -0
- package/src/arrow/index.ts +129 -0
- package/src/arrow/predicates.ts +66 -0
- package/src/arrow/repr.test-d.ts +189 -0
- package/src/arrow/schema-types.ts +213 -0
- package/src/arrow/types.ts +74 -0
- package/src/catalog/attach-option.ts +107 -0
- package/src/catalog/attach-options.ts +79 -0
- package/src/catalog/composite.ts +180 -0
- package/src/catalog/descriptors.ts +164 -0
- package/src/catalog/interface.ts +721 -0
- package/src/catalog/read-only.ts +909 -0
- package/src/client/client.ts +1348 -0
- package/src/client/errors.ts +69 -0
- package/src/client/helpers.ts +56 -0
- package/src/client/protocol.ts +33 -0
- package/src/client/types.ts +140 -0
- package/src/client-entry.ts +112 -0
- package/src/codec/asd.ts +147 -0
- package/src/crypto.ts +143 -0
- package/src/errors.ts +61 -0
- package/src/filter-pushdown/collector.ts +147 -0
- package/src/filter-pushdown/deserialize.ts +253 -0
- package/src/filter-pushdown/evaluate.ts +591 -0
- package/src/filter-pushdown/index.ts +28 -0
- package/src/filter-pushdown/types.ts +91 -0
- package/src/functions/aggregate.ts +384 -0
- package/src/functions/copy-from.ts +282 -0
- package/src/functions/copy-to.ts +319 -0
- package/src/functions/registry.ts +258 -0
- package/src/functions/scalar.ts +356 -0
- package/src/functions/storage-cf-do.ts +303 -0
- package/src/functions/storage.ts +521 -0
- package/src/functions/table-buffering.ts +562 -0
- package/src/functions/table-in-out.ts +373 -0
- package/src/functions/table.ts +521 -0
- package/src/functions/types.ts +192 -0
- package/src/generated/vgi-client.ts +560 -0
- package/src/generated/vgi-protocol-schemas.ts +864 -0
- package/src/index.core.ts +267 -0
- package/src/index.ts +11 -0
- package/src/metadata/resolve.ts +109 -0
- package/src/metadata/serialize.ts +242 -0
- package/src/metadata/types.ts +60 -0
- package/src/protocol/dispatch.ts +52 -0
- package/src/protocol/handlers/aggregate.ts +355 -0
- package/src/protocol/handlers/catalog/admin.ts +368 -0
- package/src/protocol/handlers/catalog/index.ts +25 -0
- package/src/protocol/handlers/catalog/index_methods.ts +65 -0
- package/src/protocol/handlers/catalog/macro.ts +114 -0
- package/src/protocol/handlers/catalog/shared.ts +291 -0
- package/src/protocol/handlers/catalog/table.ts +401 -0
- package/src/protocol/handlers/catalog/view.ts +119 -0
- package/src/protocol/handlers/function.ts +302 -0
- package/src/protocol/handlers/shared.ts +80 -0
- package/src/protocol/handlers/table-buffering.ts +207 -0
- package/src/protocol/serialize.ts +23 -0
- package/src/protocol/serializers/arguments.ts +160 -0
- package/src/protocol/serializers/bind.ts +212 -0
- package/src/protocol/serializers/cardinality.ts +45 -0
- package/src/protocol/serializers/init.ts +247 -0
- package/src/protocol/serializers/shared.ts +46 -0
- package/src/protocol/state-serializer.ts +181 -0
- package/src/protocol/types.ts +153 -0
- package/src/secrets/helpers.ts +70 -0
- package/src/types.ts +71 -0
- package/src/util/arrow/index.ts +23 -0
- package/src/util/bytes.ts +14 -0
- package/src/util/statistics.ts +40 -0
- package/src/worker-cf-entry.ts +172 -0
- package/src/worker.ts +212 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
// Table sink+source ("table_buffering") function implementation.
|
|
3
|
+
//
|
|
4
|
+
// Three phases, mirroring the C++ PhysicalVgiTableBuffering Sink+Source
|
|
5
|
+
// operator:
|
|
6
|
+
// 1. Sink — process(batch, params) -> state_id (bytes), one per input
|
|
7
|
+
// batch. Driven by the unary `table_buffering_process` RPC.
|
|
8
|
+
// 2. Combine — combine(state_ids, params) -> finalize_state_ids (bytes[]),
|
|
9
|
+
// once on the coordinator. Driven by `table_buffering_combine`.
|
|
10
|
+
// 3. Source — finalize(params, fid, state, out) producer-mode streaming,
|
|
11
|
+
// one batch per tick. Driven by an init() stream with
|
|
12
|
+
// phase=TABLE_BUFFERING_FINALIZE.
|
|
13
|
+
//
|
|
14
|
+
// CROSS-PROCESS INVARIANT: state written in process() that finalize() reads
|
|
15
|
+
// MUST live in cross-process storage scoped by execution_id (BoundStorage).
|
|
16
|
+
// The Source phase may route a finalize_state_id to a worker process that
|
|
17
|
+
// did NOT run the corresponding process() calls.
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
type VgiSchema,
|
|
21
|
+
schema,
|
|
22
|
+
type VgiDataType,
|
|
23
|
+
type VgiBatch,
|
|
24
|
+
nullType,
|
|
25
|
+
} from "../arrow/index.js";
|
|
26
|
+
import type { OutputCollector } from "@query-farm/vgi-rpc";
|
|
27
|
+
import { DEFAULT_MAX_WORKERS, TableInOutPhase } from "../types.js";
|
|
28
|
+
import type {
|
|
29
|
+
BindRequest,
|
|
30
|
+
BindResponse,
|
|
31
|
+
InitRequest,
|
|
32
|
+
GlobalInitResponse,
|
|
33
|
+
TableFunctionCardinalityRequest,
|
|
34
|
+
} from "../protocol/types.js";
|
|
35
|
+
import type { TableCardinality } from "../types.js";
|
|
36
|
+
import type {
|
|
37
|
+
VgiFunction,
|
|
38
|
+
FunctionMeta,
|
|
39
|
+
StreamHandlers,
|
|
40
|
+
FunctionExample,
|
|
41
|
+
} from "./types.js";
|
|
42
|
+
import type { ArgumentSpec } from "../arguments/argument-spec.js";
|
|
43
|
+
import { batchToScalarDict, batchToSecretDict, projectSchema } from "../util/arrow/index.js";
|
|
44
|
+
import { batchFromColumns, readCanonicalValue } from "../arrow/index.js";
|
|
45
|
+
import { codecFor } from "../arrow/codec/registry.js";
|
|
46
|
+
import {
|
|
47
|
+
buildJoinKeysLookup,
|
|
48
|
+
deserializeFilters,
|
|
49
|
+
FilteringOutputCollector,
|
|
50
|
+
type PushdownFilters,
|
|
51
|
+
} from "../filter-pushdown/index.js";
|
|
52
|
+
import { FunctionStability } from "../types.js";
|
|
53
|
+
import { BoundStorage, storage as defaultStorage, FrameworkNS } from "./storage.js";
|
|
54
|
+
import {
|
|
55
|
+
serializeInitRequest,
|
|
56
|
+
deserializeInitRequest,
|
|
57
|
+
serializeGlobalInitResponse,
|
|
58
|
+
} from "../protocol/serialize.js";
|
|
59
|
+
import { serializeBatch, deserializeBatch } from "../util/arrow/index.js";
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Parameter bundles
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
export interface TableBufferingBindParams<TArgs = Record<string, any>> {
|
|
66
|
+
args: TArgs;
|
|
67
|
+
bindCall: BindRequest;
|
|
68
|
+
settings: Record<string, any>;
|
|
69
|
+
secrets: Record<string, Record<string, any>>;
|
|
70
|
+
/** True on the second bind pass, after the connector resolved the secrets
|
|
71
|
+
* requested via the onBind `lookupSecret*` return fields on the first pass. */
|
|
72
|
+
resolvedSecretsProvided: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TableBufferingParams<TArgs = Record<string, any>> {
|
|
76
|
+
args: TArgs;
|
|
77
|
+
initCall: InitRequest;
|
|
78
|
+
outputSchema: VgiSchema;
|
|
79
|
+
settings: Record<string, any>;
|
|
80
|
+
secrets: Record<string, Record<string, any>>;
|
|
81
|
+
/** Shared cross-process storage scoped by execution_id. */
|
|
82
|
+
storage: BoundStorage;
|
|
83
|
+
/** Stable across coordinator + secondary workers for one DuckDB execution. */
|
|
84
|
+
executionId: Uint8Array;
|
|
85
|
+
/** Catalog attach identity (plaintext bytes). */
|
|
86
|
+
attachId: Uint8Array;
|
|
87
|
+
/** Hex/raw VGI transaction id, or null. */
|
|
88
|
+
transactionId: Uint8Array | null;
|
|
89
|
+
function_name: string;
|
|
90
|
+
/** Per-chunk batch_index when requiresInputBatchIndex=true; else null. */
|
|
91
|
+
batchIndex: number | null;
|
|
92
|
+
/** In-band log sink for the unary process()/combine() RPCs. */
|
|
93
|
+
clientLog: (level: string, message: string) => void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Functional API config
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
export interface TableBufferingConfig<
|
|
101
|
+
TArgs = Record<string, any>,
|
|
102
|
+
TState = any,
|
|
103
|
+
> {
|
|
104
|
+
name: string;
|
|
105
|
+
description?: string;
|
|
106
|
+
args?: Record<string, VgiDataType>;
|
|
107
|
+
namedArgs?: Record<string, VgiDataType>;
|
|
108
|
+
argDefaults?: Record<string, any>;
|
|
109
|
+
/** Bind: default passes through input schema. May be async. Return non-empty
|
|
110
|
+
* `lookupSecret*` lists on the first pass to request DuckDB secrets; the
|
|
111
|
+
* connector then re-binds with `resolvedSecretsProvided` and the resolved
|
|
112
|
+
* values are available in the process/finalize params' `secrets`. */
|
|
113
|
+
onBind?: (params: TableBufferingBindParams<TArgs>) =>
|
|
114
|
+
| {
|
|
115
|
+
outputSchema: VgiSchema;
|
|
116
|
+
opaqueData?: Uint8Array;
|
|
117
|
+
lookupSecretTypes?: string[];
|
|
118
|
+
lookupScopes?: string[];
|
|
119
|
+
lookupNames?: string[];
|
|
120
|
+
}
|
|
121
|
+
| Promise<{
|
|
122
|
+
outputSchema: VgiSchema;
|
|
123
|
+
opaqueData?: Uint8Array;
|
|
124
|
+
lookupSecretTypes?: string[];
|
|
125
|
+
lookupScopes?: string[];
|
|
126
|
+
lookupNames?: string[];
|
|
127
|
+
}>;
|
|
128
|
+
/** Sink: ingest one batch, return an opaque state_id. */
|
|
129
|
+
process: (
|
|
130
|
+
batch: VgiBatch,
|
|
131
|
+
params: TableBufferingParams<TArgs>,
|
|
132
|
+
) => Uint8Array | Promise<Uint8Array>;
|
|
133
|
+
/** Combine: group/merge state_ids, return finalize_state_ids. */
|
|
134
|
+
combine: (
|
|
135
|
+
stateIds: Uint8Array[],
|
|
136
|
+
params: TableBufferingParams<TArgs>,
|
|
137
|
+
) => Uint8Array[] | Promise<Uint8Array[]>;
|
|
138
|
+
/** Build the initial per-tick finalize state for a finalize_state_id. */
|
|
139
|
+
initialFinalizeState?: (
|
|
140
|
+
finalizeStateId: Uint8Array,
|
|
141
|
+
params: TableBufferingParams<TArgs>,
|
|
142
|
+
) => TState | Promise<TState>;
|
|
143
|
+
/** Source tick: emit one batch via out.emit / signal EOS via out.finish. */
|
|
144
|
+
finalize: (
|
|
145
|
+
params: TableBufferingParams<TArgs>,
|
|
146
|
+
finalizeStateId: Uint8Array,
|
|
147
|
+
state: TState,
|
|
148
|
+
out: OutputCollector,
|
|
149
|
+
) => void | Promise<void>;
|
|
150
|
+
cardinality?: (
|
|
151
|
+
params: TableBufferingBindParams<TArgs>,
|
|
152
|
+
) => TableCardinality | Promise<TableCardinality>;
|
|
153
|
+
// Metadata
|
|
154
|
+
projectionPushdown?: boolean;
|
|
155
|
+
filterPushdown?: boolean;
|
|
156
|
+
autoApplyFilters?: boolean;
|
|
157
|
+
/** Force ParallelSink=false in the C++ operator (single-thread ingest). */
|
|
158
|
+
sinkOrderDependent?: boolean;
|
|
159
|
+
/** Force serial Source drain in finalize_queue order. */
|
|
160
|
+
sourceOrderDependent?: boolean;
|
|
161
|
+
/** Thread DuckDB's per-chunk batch_index into every process() call. */
|
|
162
|
+
requiresInputBatchIndex?: boolean;
|
|
163
|
+
stability?: FunctionStability;
|
|
164
|
+
examples?: FunctionExample[];
|
|
165
|
+
categories?: string[];
|
|
166
|
+
tags?: Record<string, string>;
|
|
167
|
+
maxWorkers?: number;
|
|
168
|
+
requiredSettings?: string[];
|
|
169
|
+
requiredSecrets?: string[];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A table_buffering VgiFunction also carries its callback config so the
|
|
174
|
+
* protocol's unary handlers (process/combine/destructor) can reach it.
|
|
175
|
+
*/
|
|
176
|
+
export interface TableBufferingVgiFunction extends VgiFunction {
|
|
177
|
+
bufferingConfig: TableBufferingConfig<any, any>;
|
|
178
|
+
bufferingExtractArgs: (request: BindRequest) => any;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function defineTableBufferingFunction<
|
|
182
|
+
TArgs = Record<string, any>,
|
|
183
|
+
TState = any,
|
|
184
|
+
>(config: TableBufferingConfig<TArgs, TState>): TableBufferingVgiFunction {
|
|
185
|
+
if (config.sinkOrderDependent && config.requiresInputBatchIndex) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`${config.name}: sinkOrderDependent and requiresInputBatchIndex are ` +
|
|
188
|
+
`mutually exclusive — single-thread sink already orders input.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const specs: ArgumentSpec[] = [];
|
|
193
|
+
let posIdx = 0;
|
|
194
|
+
if (config.args) {
|
|
195
|
+
for (const [name, type] of Object.entries(config.args)) {
|
|
196
|
+
specs.push({ name, position: posIdx++, arrowType: type });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
specs.push({
|
|
200
|
+
name: "data",
|
|
201
|
+
position: posIdx++,
|
|
202
|
+
arrowType: nullType(),
|
|
203
|
+
isTableInput: true,
|
|
204
|
+
});
|
|
205
|
+
if (config.namedArgs) {
|
|
206
|
+
for (const [name, type] of Object.entries(config.namedArgs)) {
|
|
207
|
+
specs.push({ name, position: name, arrowType: type });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const meta: FunctionMeta = {
|
|
212
|
+
name: config.name,
|
|
213
|
+
description: config.description,
|
|
214
|
+
stability: config.stability,
|
|
215
|
+
projectionPushdown: config.projectionPushdown,
|
|
216
|
+
filterPushdown: config.filterPushdown,
|
|
217
|
+
autoApplyFilters: config.autoApplyFilters,
|
|
218
|
+
examples: config.examples,
|
|
219
|
+
categories: config.categories,
|
|
220
|
+
tags: config.tags,
|
|
221
|
+
maxWorkers: config.maxWorkers,
|
|
222
|
+
requiredSettings: config.requiredSettings,
|
|
223
|
+
requiredSecrets: config.requiredSecrets,
|
|
224
|
+
// The Source phase always runs (it's how output is produced), so a
|
|
225
|
+
// table_buffering function always advertises has_finalize.
|
|
226
|
+
hasFinalize: true,
|
|
227
|
+
sinkOrderDependent: config.sinkOrderDependent,
|
|
228
|
+
sourceOrderDependent: config.sourceOrderDependent,
|
|
229
|
+
requiresInputBatchIndex: config.requiresInputBatchIndex,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
function extractArgs(request: BindRequest): TArgs {
|
|
233
|
+
const args: Record<string, any> = {};
|
|
234
|
+
for (const spec of specs) {
|
|
235
|
+
if (spec.isTableInput) continue;
|
|
236
|
+
const defaultVal =
|
|
237
|
+
config.argDefaults?.[spec.name] !== undefined
|
|
238
|
+
? config.argDefaults[spec.name]
|
|
239
|
+
: undefined;
|
|
240
|
+
let val = request.arguments.get(spec.position, defaultVal);
|
|
241
|
+
if (typeof val === "bigint") val = Number(val);
|
|
242
|
+
args[spec.name] = val;
|
|
243
|
+
}
|
|
244
|
+
return args as TArgs;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const func: TableBufferingVgiFunction = {
|
|
248
|
+
kind: "table_buffering" as any,
|
|
249
|
+
meta,
|
|
250
|
+
argumentSpecs: specs,
|
|
251
|
+
bufferingConfig: config as TableBufferingConfig<any, any>,
|
|
252
|
+
bufferingExtractArgs: extractArgs as any,
|
|
253
|
+
|
|
254
|
+
async bind(request: BindRequest): Promise<BindResponse> {
|
|
255
|
+
if (config.onBind) {
|
|
256
|
+
const args = extractArgs(request);
|
|
257
|
+
const settings = batchToScalarDict(request.settings);
|
|
258
|
+
const secrets = batchToSecretDict(request.secrets);
|
|
259
|
+
const result = await config.onBind({
|
|
260
|
+
args,
|
|
261
|
+
bindCall: request,
|
|
262
|
+
settings,
|
|
263
|
+
secrets,
|
|
264
|
+
resolvedSecretsProvided: request.resolved_secrets_provided ?? false,
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
output_schema: result.outputSchema,
|
|
268
|
+
opaque_data: result.opaqueData ?? null,
|
|
269
|
+
lookup_secret_types: result.lookupSecretTypes,
|
|
270
|
+
lookup_scopes: result.lookupScopes,
|
|
271
|
+
lookup_names: result.lookupNames,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
output_schema: request.input_schema ?? schema([]),
|
|
276
|
+
opaque_data: null,
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
async globalInit(request: InitRequest): Promise<GlobalInitResponse> {
|
|
281
|
+
// Mint a stable execution_id for the primary; secondary inits reuse it.
|
|
282
|
+
let executionId: Uint8Array;
|
|
283
|
+
if (request.execution_id) {
|
|
284
|
+
executionId = request.execution_id;
|
|
285
|
+
} else {
|
|
286
|
+
executionId = new Uint8Array(16);
|
|
287
|
+
crypto.getRandomValues(executionId);
|
|
288
|
+
}
|
|
289
|
+
const response: GlobalInitResponse = {
|
|
290
|
+
max_workers: config.maxWorkers ?? DEFAULT_MAX_WORKERS,
|
|
291
|
+
execution_id: executionId,
|
|
292
|
+
opaque_data: null,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// On the sink-side init, persist init metadata so any pool worker can
|
|
296
|
+
// cold-load it when serving subsequent process()/combine() unary RPCs
|
|
297
|
+
// (which carry no init context themselves). Mirrors Python's
|
|
298
|
+
// FrameworkNS.BUFFERING_INIT state_put.
|
|
299
|
+
if (request.phase === TableInOutPhase.TABLE_BUFFERING) {
|
|
300
|
+
const bound = new BoundStorage(defaultStorage, executionId);
|
|
301
|
+
await bound.statePut(
|
|
302
|
+
FrameworkNS.BUFFERING_INIT,
|
|
303
|
+
BoundStorage.packIntKey(-1),
|
|
304
|
+
encodeBufferingInit(request, response),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return response;
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
createStreamHandlers(
|
|
311
|
+
request: InitRequest,
|
|
312
|
+
response: GlobalInitResponse,
|
|
313
|
+
_accumulatedState?: any,
|
|
314
|
+
): StreamHandlers {
|
|
315
|
+
const args = extractArgs(request.bind_call);
|
|
316
|
+
const settings = batchToScalarDict(request.bind_call.settings);
|
|
317
|
+
const secrets = batchToSecretDict(request.bind_call.secrets);
|
|
318
|
+
|
|
319
|
+
const projIds = request.projection_ids && meta.projectionPushdown
|
|
320
|
+
? request.projection_ids
|
|
321
|
+
: null;
|
|
322
|
+
const outputSchema = projIds
|
|
323
|
+
? projectSchema(projIds, request.output_schema)
|
|
324
|
+
: request.output_schema;
|
|
325
|
+
|
|
326
|
+
const joinKeysLookup = buildJoinKeysLookup(request.join_keys);
|
|
327
|
+
const pushdownFilters = request.pushdown_filters
|
|
328
|
+
? deserializeFilters(request.pushdown_filters, joinKeysLookup)
|
|
329
|
+
: undefined;
|
|
330
|
+
|
|
331
|
+
const bound = new BoundStorage(
|
|
332
|
+
defaultStorage,
|
|
333
|
+
response.execution_id ?? new Uint8Array(16),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const params: TableBufferingParams<TArgs> = {
|
|
337
|
+
args,
|
|
338
|
+
initCall: request,
|
|
339
|
+
outputSchema,
|
|
340
|
+
settings,
|
|
341
|
+
secrets,
|
|
342
|
+
storage: bound,
|
|
343
|
+
executionId: response.execution_id,
|
|
344
|
+
attachId: request.bind_call.attach_opaque_data ?? new Uint8Array(0),
|
|
345
|
+
transactionId: request.bind_call.transaction_opaque_data ?? null,
|
|
346
|
+
function_name: request.bind_call.function_name,
|
|
347
|
+
batchIndex: null,
|
|
348
|
+
clientLog: () => {},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
if (request.phase === TableInOutPhase.TABLE_BUFFERING_FINALIZE) {
|
|
352
|
+
// SOURCE phase: producer-mode streaming, one batch per tick.
|
|
353
|
+
const finalizeStateId = request.finalize_state_id ?? new Uint8Array(0);
|
|
354
|
+
return {
|
|
355
|
+
outputSchema,
|
|
356
|
+
producerInit: () => ({ state: { __tbf_uninit: true } as any }),
|
|
357
|
+
producerFn: async (
|
|
358
|
+
pState: { state: any },
|
|
359
|
+
out: OutputCollector,
|
|
360
|
+
) => {
|
|
361
|
+
// Lazily build the user's per-tick state on the first tick.
|
|
362
|
+
// pState.state round-trips through the HTTP exchange-state token
|
|
363
|
+
// between ticks; `__tbf_uninit` marks "not yet initialized".
|
|
364
|
+
let userState: any = pState.state;
|
|
365
|
+
if (userState && userState.__tbf_uninit) {
|
|
366
|
+
userState = config.initialFinalizeState
|
|
367
|
+
? await config.initialFinalizeState(finalizeStateId, params)
|
|
368
|
+
: null;
|
|
369
|
+
pState.state = userState ?? null;
|
|
370
|
+
} else {
|
|
371
|
+
userState = pState.state;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Narrow each emitted (full-width) buffered batch to the projected
|
|
375
|
+
// output schema, then auto-apply pushdown filters. Mirrors the
|
|
376
|
+
// streaming TableFunctionGenerator path: project first, filter
|
|
377
|
+
// second.
|
|
378
|
+
let wrappedOut: OutputCollector = projIds
|
|
379
|
+
? makeProjectingCollector(out, outputSchema)
|
|
380
|
+
: out;
|
|
381
|
+
if (config.autoApplyFilters && pushdownFilters) {
|
|
382
|
+
wrappedOut = new FilteringOutputCollector(wrappedOut, pushdownFilters) as unknown as OutputCollector;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
await config.finalize(params, finalizeStateId, userState, wrappedOut);
|
|
386
|
+
|
|
387
|
+
// Persist the (possibly mutated) user state for the next tick.
|
|
388
|
+
pState.state = userState ?? null;
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// SINK-init phase (TABLE_BUFFERING): the C++ side opens the init stream
|
|
394
|
+
// in exchange mode, immediately sends EOS, and drains output to EOS.
|
|
395
|
+
// We produce nothing — the real sink traffic is the separate unary
|
|
396
|
+
// table_buffering_process / _combine RPCs.
|
|
397
|
+
return {
|
|
398
|
+
outputSchema,
|
|
399
|
+
inputSchema: request.bind_call.input_schema ?? undefined,
|
|
400
|
+
exchangeInit: () => ({ state: null }),
|
|
401
|
+
exchangeFn: async (
|
|
402
|
+
_state: any,
|
|
403
|
+
_input: VgiBatch,
|
|
404
|
+
_out: OutputCollector,
|
|
405
|
+
) => {
|
|
406
|
+
// No-op: no per-batch output on the sink-init stream.
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
if (config.cardinality) {
|
|
413
|
+
func.cardinality = async (request: TableFunctionCardinalityRequest) => {
|
|
414
|
+
const args = extractArgs(request.bind_call);
|
|
415
|
+
const settings = batchToScalarDict(request.bind_call.settings);
|
|
416
|
+
const secrets = batchToSecretDict(request.bind_call.secrets);
|
|
417
|
+
return config.cardinality!({
|
|
418
|
+
args,
|
|
419
|
+
bindCall: request.bind_call,
|
|
420
|
+
settings,
|
|
421
|
+
secrets,
|
|
422
|
+
resolvedSecretsProvided: request.bind_call.resolved_secrets_provided ?? false,
|
|
423
|
+
});
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return func;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Wrap an OutputCollector so each emitted batch is projected-by-name to the
|
|
431
|
+
// target (narrowed) output schema before forwarding. Mirrors table.ts's
|
|
432
|
+
// makeProjectingCollector — buffered batches are stored full-width, so the
|
|
433
|
+
// Source phase narrows them per-tick to the projected columns.
|
|
434
|
+
function makeProjectingCollector(
|
|
435
|
+
inner: OutputCollector,
|
|
436
|
+
targetSchema: VgiSchema,
|
|
437
|
+
): OutputCollector {
|
|
438
|
+
function alreadyMatches(batch: VgiBatch): boolean {
|
|
439
|
+
if (batch.schema.fields.length !== targetSchema.fields.length) return false;
|
|
440
|
+
for (let i = 0; i < targetSchema.fields.length; i++) {
|
|
441
|
+
if (batch.schema.fields[i].name !== targetSchema.fields[i].name) return false;
|
|
442
|
+
}
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
function project(batch: VgiBatch): VgiBatch {
|
|
446
|
+
if (alreadyMatches(batch)) return batch;
|
|
447
|
+
const cols: Record<string, any[]> = {};
|
|
448
|
+
for (const f of targetSchema.fields) {
|
|
449
|
+
const src = batch.getChild(f.name);
|
|
450
|
+
if (src) {
|
|
451
|
+
// Canonical read -> rich so the rebuild goes through the codec path
|
|
452
|
+
// (backend-agnostic, lossless) rather than the raw `.get(i)`.
|
|
453
|
+
const type = f.type as VgiDataType;
|
|
454
|
+
const codec = codecFor(type);
|
|
455
|
+
const arr: any[] = [];
|
|
456
|
+
for (let i = 0; i < batch.numRows; i++) {
|
|
457
|
+
arr.push(codec.canonicalToRich(readCanonicalValue(type, src, i)));
|
|
458
|
+
}
|
|
459
|
+
cols[f.name] = arr;
|
|
460
|
+
} else {
|
|
461
|
+
cols[f.name] = new Array(batch.numRows).fill(null);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return batchFromColumns(cols, targetSchema);
|
|
465
|
+
}
|
|
466
|
+
return new Proxy(inner, {
|
|
467
|
+
get(target, prop, receiver) {
|
|
468
|
+
if (prop === "emit") {
|
|
469
|
+
return function (
|
|
470
|
+
this: OutputCollector,
|
|
471
|
+
batchOrColumns: VgiBatch | Record<string, any[]>,
|
|
472
|
+
metadata?: Map<string, string>,
|
|
473
|
+
) {
|
|
474
|
+
if (batchOrColumns && typeof (batchOrColumns as any).getChild === "function") {
|
|
475
|
+
return (target as any).emit(project(batchOrColumns as VgiBatch), metadata);
|
|
476
|
+
}
|
|
477
|
+
return (target as any).emit(batchOrColumns, metadata);
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const v = Reflect.get(target, prop, receiver);
|
|
481
|
+
return typeof v === "function" ? v.bind(target) : v;
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ============================================================================
|
|
487
|
+
// Init-metadata persistence (mirrors Python _encode/_decode_table_buffering_init)
|
|
488
|
+
// ============================================================================
|
|
489
|
+
|
|
490
|
+
export function encodeBufferingInit(
|
|
491
|
+
request: InitRequest,
|
|
492
|
+
response: GlobalInitResponse,
|
|
493
|
+
): Uint8Array {
|
|
494
|
+
// Serialize the InitRequest batch and the GlobalInitResponse so any pool
|
|
495
|
+
// worker can cold-rebuild params when serving a process()/combine() RPC.
|
|
496
|
+
const initBatch = serializeInitRequest(request);
|
|
497
|
+
const initBytes = serializeBatch(initBatch);
|
|
498
|
+
const respBatch = serializeGlobalInitResponse(response);
|
|
499
|
+
// GlobalInitResponse serializer returns a plain dict; rebuild a 1-row batch.
|
|
500
|
+
// Simpler: re-serialize as a length-prefixed pair of byte blobs.
|
|
501
|
+
const respBytes = encodeGlobalInitResponse(respBatch);
|
|
502
|
+
return concatLenPrefixed([initBytes, respBytes]);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function decodeBufferingInit(
|
|
506
|
+
payload: Uint8Array,
|
|
507
|
+
): { request: InitRequest; response: GlobalInitResponse } {
|
|
508
|
+
const [initBytes, respBytes] = splitLenPrefixed(payload, 2);
|
|
509
|
+
const initBatch = deserializeBatch(initBytes);
|
|
510
|
+
const initDict = batchToScalarDict(initBatch);
|
|
511
|
+
const request = deserializeInitRequest(initDict);
|
|
512
|
+
const response = decodeGlobalInitResponse(respBytes);
|
|
513
|
+
return { request, response };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function encodeGlobalInitResponse(dict: Record<string, any>): Uint8Array {
|
|
517
|
+
// Encode { execution_id, opaque_data, max_workers } as length-prefixed blobs.
|
|
518
|
+
const eid: Uint8Array = dict.execution_id ?? new Uint8Array(0);
|
|
519
|
+
const opaque: Uint8Array | null = dict.opaque_data ?? null;
|
|
520
|
+
const maxWorkers = Number(dict.max_workers ?? 1);
|
|
521
|
+
const mwBuf = new Uint8Array(8);
|
|
522
|
+
new DataView(mwBuf.buffer).setBigInt64(0, BigInt(maxWorkers), true);
|
|
523
|
+
return concatLenPrefixed([eid, opaque ?? new Uint8Array(0), mwBuf]);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function decodeGlobalInitResponse(bytes: Uint8Array): GlobalInitResponse {
|
|
527
|
+
const [eid, opaque, mwBuf] = splitLenPrefixed(bytes, 3);
|
|
528
|
+
const maxWorkers = Number(new DataView(mwBuf.buffer, mwBuf.byteOffset, 8).getBigInt64(0, true));
|
|
529
|
+
return {
|
|
530
|
+
execution_id: eid,
|
|
531
|
+
opaque_data: opaque.length > 0 ? opaque : null,
|
|
532
|
+
max_workers: maxWorkers,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function concatLenPrefixed(parts: Uint8Array[]): Uint8Array {
|
|
537
|
+
let total = 0;
|
|
538
|
+
for (const p of parts) total += 4 + p.length;
|
|
539
|
+
const out = new Uint8Array(total);
|
|
540
|
+
const dv = new DataView(out.buffer);
|
|
541
|
+
let off = 0;
|
|
542
|
+
for (const p of parts) {
|
|
543
|
+
dv.setUint32(off, p.length, true);
|
|
544
|
+
off += 4;
|
|
545
|
+
out.set(p, off);
|
|
546
|
+
off += p.length;
|
|
547
|
+
}
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function splitLenPrefixed(buf: Uint8Array, count: number): Uint8Array[] {
|
|
552
|
+
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
553
|
+
const parts: Uint8Array[] = [];
|
|
554
|
+
let off = 0;
|
|
555
|
+
for (let i = 0; i < count; i++) {
|
|
556
|
+
const len = dv.getUint32(off, true);
|
|
557
|
+
off += 4;
|
|
558
|
+
parts.push(buf.subarray(off, off + len));
|
|
559
|
+
off += len;
|
|
560
|
+
}
|
|
561
|
+
return parts;
|
|
562
|
+
}
|