@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,319 @@
|
|
|
1
|
+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
// Custom `COPY ... TO` format writer.
|
|
3
|
+
//
|
|
4
|
+
// A CopyToFunction lets a VGI catalog act as a remote sink: the user runs
|
|
5
|
+
// `COPY (query|table) TO 'path' (FORMAT <name>, opt val, ...)` and DuckDB
|
|
6
|
+
// streams the source rows out to the worker, which writes them to a
|
|
7
|
+
// destination. The worker reports the row count; there is no Source phase.
|
|
8
|
+
//
|
|
9
|
+
// Mechanically a COPY-TO writer is a buffered (Sink+Combine) function with NO
|
|
10
|
+
// Source phase — it reuses the entire table_buffering_process /
|
|
11
|
+
// table_buffering_combine machinery on both sides:
|
|
12
|
+
// * `write` is called once per input batch (the buffered process() step,
|
|
13
|
+
// fanned out across DuckDB's sink threads / per-thread workers). Persist the
|
|
14
|
+
// batch to a shard via `params.storage` (execution_id-scoped).
|
|
15
|
+
// * `close` is called exactly once on the coordinator worker (the buffered
|
|
16
|
+
// combine() step, driven by DuckDB's once-only copy_to_finalize). Read the
|
|
17
|
+
// shards back and perform the terminal write+flush+close of the destination.
|
|
18
|
+
//
|
|
19
|
+
// There is no finalize/drain phase, so the destination MUST be fully written and
|
|
20
|
+
// closed inside `close` — a writer that forgets leaves a silent partial file.
|
|
21
|
+
//
|
|
22
|
+
// CROSS-PROCESS INVARIANT: write() and close() may run on different worker
|
|
23
|
+
// processes (pool rotation / HTTP). Any shard state close() needs MUST live in
|
|
24
|
+
// cross-process storage scoped by params.executionId (params.storage), not on
|
|
25
|
+
// `self` / module globals.
|
|
26
|
+
//
|
|
27
|
+
// What makes it a COPY format is twofold:
|
|
28
|
+
// * it sets `meta.copyToFormat` to the SQL FORMAT identifier, and
|
|
29
|
+
// * the catalog advertises it via `copyFromFormats()` (the
|
|
30
|
+
// `catalog_copy_from_formats` RPC returns all directions), so the VGI DuckDB
|
|
31
|
+
// extension registers a DuckDB CopyFunction for it.
|
|
32
|
+
//
|
|
33
|
+
// The destination path + format arrive on the bind through `BindRequest.copy_to`
|
|
34
|
+
// (CopyToContext). The COPY options arrive as the function's normal named
|
|
35
|
+
// arguments. The source schema rides `BindRequest.input_schema`. Mirrors
|
|
36
|
+
// vgi-python's `vgi.copy_to_function.CopyToFunction`.
|
|
37
|
+
|
|
38
|
+
import { type VgiSchema, schema, type VgiBatch, type VgiDataType } from "../arrow/index.js";
|
|
39
|
+
import type { BindRequest } from "../protocol/types.js";
|
|
40
|
+
import type { FunctionMeta, FunctionExample } from "./types.js";
|
|
41
|
+
import { safeNumber } from "../util/arrow/index.js";
|
|
42
|
+
import {
|
|
43
|
+
defineTableBufferingFunction,
|
|
44
|
+
type TableBufferingParams,
|
|
45
|
+
type TableBufferingVgiFunction,
|
|
46
|
+
} from "./table-buffering.js";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A single secret to resolve via the two-phase secret bind, returned from a COPY
|
|
50
|
+
* format's `onSecrets` hook. Mirrors a vgi-python `SecretLookupEntry`.
|
|
51
|
+
*/
|
|
52
|
+
export interface CopySecretLookup {
|
|
53
|
+
/** DuckDB secret type to resolve (e.g. `"s3"`, `"vgi_example"`). */
|
|
54
|
+
secretType: string;
|
|
55
|
+
/** Optional scope for longest-prefix matching — typically the COPY path. */
|
|
56
|
+
scope?: string;
|
|
57
|
+
/** Optional secret name for name-based lookup. */
|
|
58
|
+
name?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Declaration of a single COPY option (a named function argument). */
|
|
62
|
+
export interface CopyToOption {
|
|
63
|
+
/** Arrow type of the option value (e.g. `utf8()`, `int64()`). */
|
|
64
|
+
type: VgiDataType;
|
|
65
|
+
/** Per-option description, surfaced by `vgi_copy_formats()`. */
|
|
66
|
+
doc?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Default value. When omitted the option is REQUIRED — the worker throws a
|
|
69
|
+
* clear error at COPY bind if the user does not supply it. Mirrors
|
|
70
|
+
* vgi-python's `Arg(..., default=...)` (no default => required).
|
|
71
|
+
*/
|
|
72
|
+
default?: unknown;
|
|
73
|
+
/**
|
|
74
|
+
* Optional allowed value set, validated worker-side at bind. Mirrors
|
|
75
|
+
* vgi-python's `Arg(..., choices=[...])`.
|
|
76
|
+
*/
|
|
77
|
+
choices?: unknown[];
|
|
78
|
+
/**
|
|
79
|
+
* Optional inclusive lower bound for numeric options, validated worker-side at
|
|
80
|
+
* bind. Mirrors vgi-python's `Arg(..., ge=...)`.
|
|
81
|
+
*/
|
|
82
|
+
ge?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Optional inclusive upper bound for numeric options, validated worker-side at
|
|
85
|
+
* bind. Mirrors vgi-python's `Arg(..., le=...)`.
|
|
86
|
+
*/
|
|
87
|
+
le?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Parameters for the per-batch `write` hook. */
|
|
91
|
+
export interface CopyToWriteParams<TArgs = Record<string, unknown>> {
|
|
92
|
+
/** One input batch from the COPY source. */
|
|
93
|
+
batch: VgiBatch;
|
|
94
|
+
/** Parsed COPY options (defaults applied). */
|
|
95
|
+
options: TArgs;
|
|
96
|
+
/** Destination path from the `COPY ... TO 'path'` statement. */
|
|
97
|
+
filePath: string;
|
|
98
|
+
/** Full buffering parameters (settings, secrets, storage, executionId). */
|
|
99
|
+
params: TableBufferingParams<TArgs>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Parameters for the terminal `close` hook. */
|
|
103
|
+
export interface CopyToCloseParams<TArgs = Record<string, unknown>> {
|
|
104
|
+
/** Parsed COPY options (defaults applied). */
|
|
105
|
+
options: TArgs;
|
|
106
|
+
/** Destination path from the `COPY ... TO 'path'` statement. */
|
|
107
|
+
filePath: string;
|
|
108
|
+
/** Full buffering parameters (settings, secrets, storage, executionId). */
|
|
109
|
+
params: TableBufferingParams<TArgs>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface CopyToFunctionConfig<TArgs = Record<string, unknown>> {
|
|
113
|
+
/** Handler name (the function's registered name; `Meta.name`). */
|
|
114
|
+
name: string;
|
|
115
|
+
/** SQL `FORMAT` identifier users type, e.g. `COPY t TO 'x' (FORMAT myfmt)`. */
|
|
116
|
+
format: string;
|
|
117
|
+
/** Function description (intrinsic documentation; `Meta.description`). */
|
|
118
|
+
description?: string;
|
|
119
|
+
/** Optional free-text comment surfaced by `vgi_copy_formats()`. */
|
|
120
|
+
comment?: string | null;
|
|
121
|
+
/** COPY direction; only `"to"` is supported here. */
|
|
122
|
+
direction?: string;
|
|
123
|
+
/**
|
|
124
|
+
* When true, the writer requires rows in source order — discovery advertises
|
|
125
|
+
* `ordered=true` and the extension uses a single-threaded sink
|
|
126
|
+
* (`REGULAR_COPY_TO_FILE`) so one worker receives every batch in source
|
|
127
|
+
* order. Mirrors vgi-python's `Meta.sink_order_dependent`.
|
|
128
|
+
*/
|
|
129
|
+
ordered?: boolean;
|
|
130
|
+
/** COPY options, keyed by option name. The `file_path` is NOT an option. */
|
|
131
|
+
options?: Record<string, CopyToOption>;
|
|
132
|
+
categories?: string[];
|
|
133
|
+
tags?: Record<string, string>;
|
|
134
|
+
examples?: FunctionExample[];
|
|
135
|
+
requiredSettings?: string[];
|
|
136
|
+
requiredSecrets?: string[];
|
|
137
|
+
/**
|
|
138
|
+
* Optional secret-bind hook: forward CREATE SECRET credentials for
|
|
139
|
+
* secret-backed cloud writes (S3/GCS/HTTP/…). Called during bind (only on the
|
|
140
|
+
* first pass); return the secrets to resolve — typically scoped by the
|
|
141
|
+
* destination `filePath`. The framework's two-phase secret bind resolves each
|
|
142
|
+
* lookup from the caller's SecretManager and surfaces the resolved values on
|
|
143
|
+
* `params.secrets` at `write`/`close` time. Mirrors vgi-python's
|
|
144
|
+
* `CopyToFunction.on_secrets`.
|
|
145
|
+
*/
|
|
146
|
+
onSecrets?: (params: {
|
|
147
|
+
options: TArgs;
|
|
148
|
+
filePath: string;
|
|
149
|
+
bindCall: BindRequest;
|
|
150
|
+
}) => CopySecretLookup[] | void;
|
|
151
|
+
/** Persist one input `batch` to a shard (called per sink batch). */
|
|
152
|
+
write: (params: CopyToWriteParams<TArgs>) => void | Promise<void>;
|
|
153
|
+
/**
|
|
154
|
+
* Write the destination and close it, once (called on the coordinator). Read
|
|
155
|
+
* the shards persisted by `write` and perform the terminal write. Called even
|
|
156
|
+
* for empty input. Return the row count (informational).
|
|
157
|
+
*/
|
|
158
|
+
close: (params: CopyToCloseParams<TArgs>) => number | void | Promise<number | void>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Define a custom `COPY ... TO` format writer, returned as a
|
|
163
|
+
* `TableBufferingVgiFunction` (kind `"table_buffering"`) carrying the
|
|
164
|
+
* `copyToFormat` metadata marker. Register it in the catalog's function list
|
|
165
|
+
* like any function; the catalog's `copyFromFormats()` introspection picks it
|
|
166
|
+
* up and advertises the format (direction `"to"`). The worker's
|
|
167
|
+
* `table_buffering_process` / `table_buffering_combine` RPCs drive write/close.
|
|
168
|
+
*/
|
|
169
|
+
export function defineCopyToFunction<TArgs = Record<string, unknown>>(
|
|
170
|
+
config: CopyToFunctionConfig<TArgs>,
|
|
171
|
+
): TableBufferingVgiFunction {
|
|
172
|
+
const options = config.options ?? {};
|
|
173
|
+
|
|
174
|
+
// Named-argument types + defaults for the buffering function. COPY passes
|
|
175
|
+
// options by name, so each option is a named argument.
|
|
176
|
+
const namedArgs: Record<string, VgiDataType> = {};
|
|
177
|
+
const argDefaults: Record<string, unknown> = {};
|
|
178
|
+
for (const [name, opt] of Object.entries(options)) {
|
|
179
|
+
namedArgs[name] = opt.type;
|
|
180
|
+
if ("default" in opt) argDefaults[name] = opt.default;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Extract + validate the COPY options. Required options (no default) must be
|
|
184
|
+
// present; `choices` are enforced worker-side. Mirrors copy-from.ts.
|
|
185
|
+
function extractOptions(request: BindRequest): TArgs {
|
|
186
|
+
const args: Record<string, unknown> = {};
|
|
187
|
+
for (const [name, opt] of Object.entries(options)) {
|
|
188
|
+
const hasDefault = "default" in opt;
|
|
189
|
+
let val: unknown;
|
|
190
|
+
try {
|
|
191
|
+
val = request.arguments.get(name, hasDefault ? opt.default : undefined);
|
|
192
|
+
} catch {
|
|
193
|
+
val = hasDefault ? opt.default : undefined;
|
|
194
|
+
}
|
|
195
|
+
if (val === undefined || val === null) {
|
|
196
|
+
if (!hasDefault) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`COPY (FORMAT ${config.format}): missing required option '${name}'`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
val = opt.default;
|
|
202
|
+
}
|
|
203
|
+
if (typeof val === "bigint") val = safeNumber(val);
|
|
204
|
+
if (opt.choices && !opt.choices.includes(val)) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`COPY (FORMAT ${config.format}): option '${name}' value ${JSON.stringify(val)} ` +
|
|
207
|
+
`is not one of ${JSON.stringify(opt.choices)}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (typeof val === "number") {
|
|
211
|
+
if (opt.ge !== undefined && val < opt.ge) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`COPY (FORMAT ${config.format}): option '${name}' value ${val} ` +
|
|
214
|
+
`is below the minimum ${opt.ge}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (opt.le !== undefined && val > opt.le) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`COPY (FORMAT ${config.format}): option '${name}' value ${val} ` +
|
|
220
|
+
`is above the maximum ${opt.le}`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
args[name] = val;
|
|
225
|
+
}
|
|
226
|
+
return args as TArgs;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function requireCopyTo(params: TableBufferingParams<TArgs>): string {
|
|
230
|
+
const ct = params.initCall.bind_call.copy_to;
|
|
231
|
+
if (ct == null) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`${config.name} is a COPY TO format writer; invoke it via ` +
|
|
234
|
+
`COPY <source> TO '<path>' (FORMAT ${config.format}), not as a function.`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return ct.file_path;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const func = defineTableBufferingFunction<TArgs, null>({
|
|
241
|
+
name: config.name,
|
|
242
|
+
description: config.description,
|
|
243
|
+
namedArgs,
|
|
244
|
+
argDefaults,
|
|
245
|
+
categories: config.categories,
|
|
246
|
+
tags: config.tags,
|
|
247
|
+
examples: config.examples,
|
|
248
|
+
requiredSettings: config.requiredSettings,
|
|
249
|
+
requiredSecrets: config.requiredSecrets,
|
|
250
|
+
// A sink produces no rows — bind to an empty output schema. Validate the
|
|
251
|
+
// options eagerly so a missing/invalid option fails at COPY bind. If the
|
|
252
|
+
// writer declared an onSecrets hook, forward its requested lookups on the
|
|
253
|
+
// first bind pass so the two-phase secret bind resolves them (the resolved
|
|
254
|
+
// values reach write/close via params.secrets).
|
|
255
|
+
onBind: ({ bindCall, resolvedSecretsProvided }) => {
|
|
256
|
+
const opts = extractOptions(bindCall);
|
|
257
|
+
if (config.onSecrets && !resolvedSecretsProvided) {
|
|
258
|
+
const lookups =
|
|
259
|
+
config.onSecrets({
|
|
260
|
+
options: opts,
|
|
261
|
+
filePath: bindCall.copy_to?.file_path ?? "",
|
|
262
|
+
bindCall,
|
|
263
|
+
}) ?? [];
|
|
264
|
+
if (lookups.length > 0) {
|
|
265
|
+
return {
|
|
266
|
+
outputSchema: schema([]),
|
|
267
|
+
lookupSecretTypes: lookups.map((l) => l.secretType),
|
|
268
|
+
lookupScopes: lookups.map((l) => l.scope ?? ""),
|
|
269
|
+
lookupNames: lookups.map((l) => l.name ?? ""),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return { outputSchema: schema([]) };
|
|
274
|
+
},
|
|
275
|
+
// SINK: persist one batch to a shard, bucket by execution_id.
|
|
276
|
+
process: async (batch, params) => {
|
|
277
|
+
const opts = extractOptions(params.initCall.bind_call);
|
|
278
|
+
await config.write({
|
|
279
|
+
batch,
|
|
280
|
+
options: opts,
|
|
281
|
+
filePath: requireCopyTo(params),
|
|
282
|
+
params,
|
|
283
|
+
});
|
|
284
|
+
return params.executionId;
|
|
285
|
+
},
|
|
286
|
+
// COMBINE: terminal write+close, once on the coordinator. No finalize ids,
|
|
287
|
+
// so the COPY path never drains output.
|
|
288
|
+
combine: async (_stateIds, params) => {
|
|
289
|
+
const opts = extractOptions(params.initCall.bind_call);
|
|
290
|
+
await config.close({
|
|
291
|
+
options: opts,
|
|
292
|
+
filePath: requireCopyTo(params),
|
|
293
|
+
params,
|
|
294
|
+
});
|
|
295
|
+
return [];
|
|
296
|
+
},
|
|
297
|
+
// Never invoked on the COPY-TO path (combine returns no finalize ids).
|
|
298
|
+
finalize: (_params, _fid, _state, out) => {
|
|
299
|
+
out.finish();
|
|
300
|
+
},
|
|
301
|
+
// Ordered writers need a single-thread sink (REGULAR_COPY_TO_FILE).
|
|
302
|
+
sinkOrderDependent: config.ordered === true,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Decorate the function's metadata with the COPY-TO format markers + attach
|
|
306
|
+
// per-option docs to the argument specs so `vgi_copy_formats()` surfaces the
|
|
307
|
+
// option descriptions (the buffering builder doesn't carry option docs).
|
|
308
|
+
const meta = func.meta as FunctionMeta;
|
|
309
|
+
meta.copyToFormat = config.format;
|
|
310
|
+
meta.copyToDirection = config.direction ?? "to";
|
|
311
|
+
meta.copyToComment = config.comment ?? null;
|
|
312
|
+
for (const spec of func.argumentSpecs) {
|
|
313
|
+
if (spec.isTableInput) continue;
|
|
314
|
+
const opt = options[spec.name];
|
|
315
|
+
if (opt?.doc) spec.doc = opt.doc;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return func;
|
|
319
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
// Function registry: name -> VgiFunction lookup with overload resolution.
|
|
3
|
+
|
|
4
|
+
import { type VgiDataType, isBinary, isBool, isDecimal, isFloat, isInt, isNull, isUtf8 } from "../arrow/index.js";
|
|
5
|
+
import type { VgiSchema } from "../arrow/index.js";
|
|
6
|
+
import type { VgiFunction } from "./types.js";
|
|
7
|
+
import type { Arguments } from "../arguments/arguments.js";
|
|
8
|
+
import type { ArgumentSpec } from "../arguments/argument-spec.js";
|
|
9
|
+
import { FunctionNotFoundError } from "../errors.js";
|
|
10
|
+
|
|
11
|
+
export interface OverloadContext {
|
|
12
|
+
arguments?: Arguments;
|
|
13
|
+
inputSchema?: VgiSchema | null;
|
|
14
|
+
isScalar?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const EXACT_MATCH_SCORE = 2;
|
|
18
|
+
const FAMILY_MATCH_SCORE = 1;
|
|
19
|
+
|
|
20
|
+
function typesCompatible(actual: VgiDataType, declared: VgiDataType): boolean {
|
|
21
|
+
if (actual.toString() === declared.toString()) return true;
|
|
22
|
+
// Integer family
|
|
23
|
+
if (isInt(actual) && isInt(declared)) return true;
|
|
24
|
+
// Float/decimal family
|
|
25
|
+
if ((isFloat(actual) || isDecimal(actual)) &&
|
|
26
|
+
(isFloat(declared) || isDecimal(declared))) return true;
|
|
27
|
+
// String family (Utf8, LargeUtf8)
|
|
28
|
+
if (isUtf8(actual) && isUtf8(declared)) return true;
|
|
29
|
+
// Binary family
|
|
30
|
+
if (isBinary(actual) && isBinary(declared)) return true;
|
|
31
|
+
// Boolean
|
|
32
|
+
if (isBool(actual) && isBool(declared)) return true;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function scoreTypes(
|
|
37
|
+
specs: ArgumentSpec[],
|
|
38
|
+
actualTypes: (VgiDataType | null)[],
|
|
39
|
+
): { score: number; matched: boolean } {
|
|
40
|
+
let score = 0;
|
|
41
|
+
let varArgsSpec: ArgumentSpec | null = null;
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < specs.length; i++) {
|
|
44
|
+
const spec = specs[i];
|
|
45
|
+
if (spec.isVarargs) varArgsSpec = spec;
|
|
46
|
+
if (i >= actualTypes.length) break;
|
|
47
|
+
if (spec.isAnyType || isNull(spec.arrowType)) continue;
|
|
48
|
+
const actual = actualTypes[i];
|
|
49
|
+
if (actual === null) continue;
|
|
50
|
+
if (actual.toString() === spec.arrowType.toString()) {
|
|
51
|
+
score += EXACT_MATCH_SCORE;
|
|
52
|
+
} else if (typesCompatible(actual, spec.arrowType)) {
|
|
53
|
+
score += FAMILY_MATCH_SCORE;
|
|
54
|
+
} else {
|
|
55
|
+
return { score, matched: false };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Score varargs tail beyond declared specs
|
|
60
|
+
if (varArgsSpec && !varArgsSpec.isAnyType && !isNull(varArgsSpec.arrowType)) {
|
|
61
|
+
for (let i = specs.length; i < actualTypes.length; i++) {
|
|
62
|
+
const actual = actualTypes[i];
|
|
63
|
+
if (actual === null) continue;
|
|
64
|
+
if (actual.toString() === varArgsSpec.arrowType.toString()) {
|
|
65
|
+
score += EXACT_MATCH_SCORE;
|
|
66
|
+
} else if (typesCompatible(actual, varArgsSpec.arrowType)) {
|
|
67
|
+
score += FAMILY_MATCH_SCORE;
|
|
68
|
+
} else {
|
|
69
|
+
return { score, matched: false };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { score, matched: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function filterByArgumentTypes(
|
|
78
|
+
candidates: VgiFunction[],
|
|
79
|
+
args: Arguments,
|
|
80
|
+
inputSchema: VgiSchema | null | undefined,
|
|
81
|
+
isScalar: boolean,
|
|
82
|
+
): VgiFunction[] {
|
|
83
|
+
const scored: { score: number; func: VgiFunction }[] = [];
|
|
84
|
+
|
|
85
|
+
for (const func of candidates) {
|
|
86
|
+
const specs = func.argumentSpecs;
|
|
87
|
+
let score = 0;
|
|
88
|
+
let matched = true;
|
|
89
|
+
|
|
90
|
+
if (isScalar) {
|
|
91
|
+
const constSpecs = specs.filter(s => s.isConst);
|
|
92
|
+
const colSpecs = specs.filter(s => !s.isConst && typeof s.position === "number");
|
|
93
|
+
|
|
94
|
+
// Score const specs against positional argument types
|
|
95
|
+
const constTypes: (VgiDataType | null)[] = [];
|
|
96
|
+
const argsSchema = args.argumentsSchema;
|
|
97
|
+
for (let i = 0; i < args.positional.length; i++) {
|
|
98
|
+
const field = argsSchema?.fields.find(f => f.name === `positional_${i}`);
|
|
99
|
+
constTypes.push(field ? field.type : null);
|
|
100
|
+
}
|
|
101
|
+
const r1 = scoreTypes(constSpecs, constTypes);
|
|
102
|
+
score += r1.score;
|
|
103
|
+
matched = r1.matched;
|
|
104
|
+
|
|
105
|
+
// Score column specs against inputSchema
|
|
106
|
+
if (matched && inputSchema) {
|
|
107
|
+
const colTypes: (VgiDataType | null)[] = [];
|
|
108
|
+
let varArgsColSpec: ArgumentSpec | null = null;
|
|
109
|
+
for (const spec of colSpecs) {
|
|
110
|
+
if (spec.isVarargs) varArgsColSpec = spec;
|
|
111
|
+
const pos = spec.position as number;
|
|
112
|
+
colTypes.push(pos < inputSchema.fields.length ? inputSchema.fields[pos].type : null);
|
|
113
|
+
}
|
|
114
|
+
// Append varargs tail from inputSchema
|
|
115
|
+
if (varArgsColSpec) {
|
|
116
|
+
const start = (varArgsColSpec.position as number) + 1;
|
|
117
|
+
for (let i = start; i < inputSchema.fields.length; i++) {
|
|
118
|
+
colTypes.push(inputSchema.fields[i].type);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const r2 = scoreTypes(colSpecs, colTypes);
|
|
122
|
+
score += r2.score;
|
|
123
|
+
matched = r2.matched;
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// Table functions: compare positional arg types
|
|
127
|
+
const posSpecs = specs
|
|
128
|
+
.filter(s => typeof s.position === "number" && !s.isTableInput)
|
|
129
|
+
.sort((a, b) => (a.position as number) - (b.position as number));
|
|
130
|
+
|
|
131
|
+
const posTypes: (VgiDataType | null)[] = [];
|
|
132
|
+
const argsSchema = args.argumentsSchema;
|
|
133
|
+
for (let i = 0; i < args.positional.length; i++) {
|
|
134
|
+
const field = argsSchema?.fields.find(f => f.name === `positional_${i}`);
|
|
135
|
+
posTypes.push(field ? field.type : null);
|
|
136
|
+
}
|
|
137
|
+
const r = scoreTypes(posSpecs, posTypes);
|
|
138
|
+
score += r.score;
|
|
139
|
+
matched = r.matched;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (matched) {
|
|
143
|
+
scored.push({ score, func });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (scored.length === 0) return [];
|
|
148
|
+
const maxScore = Math.max(...scored.map(s => s.score));
|
|
149
|
+
return scored.filter(s => s.score === maxScore).map(s => s.func);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export class FunctionRegistry {
|
|
153
|
+
private _functions: Map<string, VgiFunction[]> = new Map();
|
|
154
|
+
|
|
155
|
+
register(func: VgiFunction): void {
|
|
156
|
+
const name = func.meta.name;
|
|
157
|
+
if (!this._functions.has(name)) {
|
|
158
|
+
this._functions.set(name, []);
|
|
159
|
+
}
|
|
160
|
+
this._functions.get(name)!.push(func);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get(name: string, context?: OverloadContext): VgiFunction {
|
|
164
|
+
const candidates = this._functions.get(name);
|
|
165
|
+
if (!candidates || candidates.length === 0) {
|
|
166
|
+
throw new FunctionNotFoundError(name, this.names());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Fast path: single candidate
|
|
170
|
+
if (candidates.length === 1) {
|
|
171
|
+
return candidates[0];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// No context: return first (backward compat)
|
|
175
|
+
if (!context?.arguments) {
|
|
176
|
+
return candidates[0];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const args = context.arguments;
|
|
180
|
+
const isScalar = context.isScalar ?? (candidates[0].kind === "scalar");
|
|
181
|
+
|
|
182
|
+
// Stage 1: Count-based filtering
|
|
183
|
+
let matches: VgiFunction[];
|
|
184
|
+
if (isScalar) {
|
|
185
|
+
// For scalars, count const specs and match against args.positional.length
|
|
186
|
+
const numConstArgs = args.positional.length;
|
|
187
|
+
matches = candidates.filter(func => {
|
|
188
|
+
const constSpecs = func.argumentSpecs.filter(s => s.isConst);
|
|
189
|
+
const hasVarargs = constSpecs.some(s => s.isVarargs);
|
|
190
|
+
const nonVarargConst = constSpecs.filter(s => !s.isVarargs).length;
|
|
191
|
+
if (hasVarargs) {
|
|
192
|
+
return numConstArgs >= nonVarargConst;
|
|
193
|
+
}
|
|
194
|
+
return constSpecs.length === numConstArgs;
|
|
195
|
+
});
|
|
196
|
+
// If count filtering didn't help, also try matching by column count
|
|
197
|
+
if (matches.length === 0 || matches.length === candidates.length) {
|
|
198
|
+
// Fall back to all candidates for type-based filtering
|
|
199
|
+
matches = candidates;
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
// For table functions, count positional specs
|
|
203
|
+
const numArgs = args.positional.length;
|
|
204
|
+
matches = candidates.filter(func => {
|
|
205
|
+
const posSpecs = func.argumentSpecs.filter(
|
|
206
|
+
s => typeof s.position === "number" && !s.isTableInput
|
|
207
|
+
);
|
|
208
|
+
const hasVarargs = posSpecs.some(s => s.isVarargs);
|
|
209
|
+
const nonVarargs = posSpecs.filter(s => !s.isVarargs).length;
|
|
210
|
+
if (hasVarargs) {
|
|
211
|
+
return numArgs >= nonVarargs;
|
|
212
|
+
}
|
|
213
|
+
return posSpecs.length === numArgs;
|
|
214
|
+
});
|
|
215
|
+
if (matches.length === 0) {
|
|
216
|
+
matches = candidates;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (matches.length === 1) {
|
|
221
|
+
return matches[0];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Stage 2: Type-based filtering
|
|
225
|
+
const typeFiltered = filterByArgumentTypes(
|
|
226
|
+
matches,
|
|
227
|
+
args,
|
|
228
|
+
context.inputSchema,
|
|
229
|
+
isScalar,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
if (typeFiltered.length === 1) {
|
|
233
|
+
return typeFiltered[0];
|
|
234
|
+
}
|
|
235
|
+
if (typeFiltered.length > 1) {
|
|
236
|
+
return typeFiltered[0]; // Best score wins
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Fallback: return first candidate
|
|
240
|
+
return candidates[0];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
has(name: string): boolean {
|
|
244
|
+
return this._functions.has(name) && this._functions.get(name)!.length > 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
names(): string[] {
|
|
248
|
+
return [...this._functions.keys()].sort();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
all(): VgiFunction[] {
|
|
252
|
+
const result: VgiFunction[] = [];
|
|
253
|
+
for (const candidates of this._functions.values()) {
|
|
254
|
+
result.push(...candidates);
|
|
255
|
+
}
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
}
|