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