@query-farm/vgi 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/LICENSE +134 -0
  2. package/README.md +494 -0
  3. package/dist/__dbg.test-d.d.ts +1 -0
  4. package/dist/arguments/argument-spec.d.ts +51 -0
  5. package/dist/arguments/arguments.d.ts +13 -0
  6. package/dist/arrow/codec/branded.d.ts +83 -0
  7. package/dist/arrow/codec/registry.d.ts +29 -0
  8. package/dist/arrow/codec/repr.d.ts +23 -0
  9. package/dist/arrow/codec/type-descriptors.d.ts +109 -0
  10. package/dist/arrow/impl-arrowjs/build.d.ts +19 -0
  11. package/dist/arrow/impl-arrowjs/canonical.d.ts +15 -0
  12. package/dist/arrow/impl-arrowjs/empty.d.ts +7 -0
  13. package/dist/arrow/impl-arrowjs/filter.d.ts +11 -0
  14. package/dist/arrow/impl-arrowjs/index.d.ts +11 -0
  15. package/dist/arrow/impl-arrowjs/ipc.d.ts +24 -0
  16. package/dist/arrow/impl-arrowjs/iterate.d.ts +39 -0
  17. package/dist/arrow/impl-arrowjs/project.d.ts +10 -0
  18. package/dist/arrow/impl-arrowjs/schema.d.ts +62 -0
  19. package/dist/arrow/impl-arrowjs/statistics.d.ts +13 -0
  20. package/dist/arrow/impl-flechette/build.d.ts +14 -0
  21. package/dist/arrow/impl-flechette/canonical.d.ts +18 -0
  22. package/dist/arrow/impl-flechette/empty.d.ts +9 -0
  23. package/dist/arrow/impl-flechette/filter.d.ts +10 -0
  24. package/dist/arrow/impl-flechette/index.d.ts +11 -0
  25. package/dist/arrow/impl-flechette/ipc.d.ts +33 -0
  26. package/dist/arrow/impl-flechette/iterate.d.ts +29 -0
  27. package/dist/arrow/impl-flechette/message-meta.d.ts +10 -0
  28. package/dist/arrow/impl-flechette/normalize-type.d.ts +11 -0
  29. package/dist/arrow/impl-flechette/project.d.ts +3 -0
  30. package/dist/arrow/impl-flechette/schema.d.ts +47 -0
  31. package/dist/arrow/impl-flechette/statistics.d.ts +13 -0
  32. package/dist/arrow/index.d.ts +11 -0
  33. package/dist/arrow/predicates.d.ts +47 -0
  34. package/dist/arrow/repr.test-d.d.ts +52 -0
  35. package/dist/arrow/schema-types.d.ts +71 -0
  36. package/dist/arrow/types.d.ts +57 -0
  37. package/dist/catalog/attach-option.d.ts +33 -0
  38. package/dist/catalog/attach-options.d.ts +30 -0
  39. package/dist/catalog/composite.d.ts +33 -0
  40. package/dist/catalog/descriptors.d.ts +154 -0
  41. package/dist/catalog/interface.d.ts +239 -0
  42. package/dist/catalog/read-only.d.ts +33 -0
  43. package/dist/client/client.d.ts +189 -0
  44. package/dist/client/errors.d.ts +26 -0
  45. package/dist/client/helpers.d.ts +12 -0
  46. package/dist/client/protocol.d.ts +16 -0
  47. package/dist/client/types.d.ts +125 -0
  48. package/dist/client-entry.d.ts +11 -0
  49. package/dist/client-entry.js +48 -0
  50. package/dist/client-entry.js.map +9 -0
  51. package/dist/codec/asd.d.ts +25 -0
  52. package/dist/crypto.d.ts +36 -0
  53. package/dist/errors.d.ts +24 -0
  54. package/dist/filter-pushdown/collector.d.ts +32 -0
  55. package/dist/filter-pushdown/deserialize.d.ts +16 -0
  56. package/dist/filter-pushdown/evaluate.d.ts +51 -0
  57. package/dist/filter-pushdown/index.d.ts +4 -0
  58. package/dist/filter-pushdown/types.d.ts +84 -0
  59. package/dist/functions/aggregate.d.ts +129 -0
  60. package/dist/functions/copy-from.d.ts +81 -0
  61. package/dist/functions/copy-to.d.ts +121 -0
  62. package/dist/functions/registry.d.ts +16 -0
  63. package/dist/functions/scalar.d.ts +98 -0
  64. package/dist/functions/storage-cf-do.d.ts +47 -0
  65. package/dist/functions/storage.d.ts +86 -0
  66. package/dist/functions/table-buffering.d.ts +99 -0
  67. package/dist/functions/table-in-out.d.ts +64 -0
  68. package/dist/functions/table.d.ts +118 -0
  69. package/dist/functions/types.d.ts +157 -0
  70. package/dist/generated/vgi-client.d.ts +466 -0
  71. package/dist/generated/vgi-protocol-schemas.d.ts +115 -0
  72. package/dist/index.core.d.ts +42 -0
  73. package/dist/index.d.ts +2 -0
  74. package/dist/index.js +10888 -0
  75. package/dist/index.js.map +85 -0
  76. package/dist/metadata/resolve.d.ts +3 -0
  77. package/dist/metadata/serialize.d.ts +12 -0
  78. package/dist/metadata/types.d.ts +47 -0
  79. package/dist/protocol/dispatch.d.ts +23 -0
  80. package/dist/protocol/handlers/aggregate.d.ts +3 -0
  81. package/dist/protocol/handlers/catalog/admin.d.ts +3 -0
  82. package/dist/protocol/handlers/catalog/index.d.ts +3 -0
  83. package/dist/protocol/handlers/catalog/index_methods.d.ts +3 -0
  84. package/dist/protocol/handlers/catalog/macro.d.ts +3 -0
  85. package/dist/protocol/handlers/catalog/shared.d.ts +70 -0
  86. package/dist/protocol/handlers/catalog/table.d.ts +3 -0
  87. package/dist/protocol/handlers/catalog/view.d.ts +3 -0
  88. package/dist/protocol/handlers/function.d.ts +8 -0
  89. package/dist/protocol/handlers/shared.d.ts +31 -0
  90. package/dist/protocol/handlers/table-buffering.d.ts +3 -0
  91. package/dist/protocol/serialize.d.ts +4 -0
  92. package/dist/protocol/serializers/arguments.d.ts +3 -0
  93. package/dist/protocol/serializers/bind.d.ts +9 -0
  94. package/dist/protocol/serializers/cardinality.d.ts +7 -0
  95. package/dist/protocol/serializers/init.d.ts +9 -0
  96. package/dist/protocol/serializers/shared.d.ts +12 -0
  97. package/dist/protocol/state-serializer.d.ts +23 -0
  98. package/dist/protocol/types.d.ts +129 -0
  99. package/dist/secrets/helpers.d.ts +15 -0
  100. package/dist/types.d.ts +52 -0
  101. package/dist/util/arrow/index.d.ts +1 -0
  102. package/dist/util/bytes.d.ts +2 -0
  103. package/dist/util/statistics.d.ts +7 -0
  104. package/dist/worker-cf-entry.d.ts +30 -0
  105. package/dist/worker-cf-entry.js +8767 -0
  106. package/dist/worker-cf-entry.js.map +74 -0
  107. package/dist/worker.d.ts +36 -0
  108. package/package.json +96 -0
  109. package/src/arguments/argument-spec.ts +200 -0
  110. package/src/arguments/arguments.ts +81 -0
  111. package/src/arrow/codec/branded.ts +206 -0
  112. package/src/arrow/codec/registry.ts +674 -0
  113. package/src/arrow/codec/repr.ts +181 -0
  114. package/src/arrow/codec/type-descriptors.ts +137 -0
  115. package/src/arrow/impl-arrowjs/build.ts +88 -0
  116. package/src/arrow/impl-arrowjs/canonical.ts +509 -0
  117. package/src/arrow/impl-arrowjs/empty.ts +72 -0
  118. package/src/arrow/impl-arrowjs/filter.ts +53 -0
  119. package/src/arrow/impl-arrowjs/index.ts +54 -0
  120. package/src/arrow/impl-arrowjs/ipc.ts +67 -0
  121. package/src/arrow/impl-arrowjs/iterate.ts +159 -0
  122. package/src/arrow/impl-arrowjs/project.ts +73 -0
  123. package/src/arrow/impl-arrowjs/schema.ts +256 -0
  124. package/src/arrow/impl-arrowjs/statistics.ts +185 -0
  125. package/src/arrow/impl-flechette/build.ts +65 -0
  126. package/src/arrow/impl-flechette/canonical.ts +289 -0
  127. package/src/arrow/impl-flechette/empty.ts +20 -0
  128. package/src/arrow/impl-flechette/filter.ts +42 -0
  129. package/src/arrow/impl-flechette/index.ts +54 -0
  130. package/src/arrow/impl-flechette/ipc.ts +110 -0
  131. package/src/arrow/impl-flechette/iterate.ts +121 -0
  132. package/src/arrow/impl-flechette/message-meta.ts +175 -0
  133. package/src/arrow/impl-flechette/normalize-type.ts +99 -0
  134. package/src/arrow/impl-flechette/project.ts +64 -0
  135. package/src/arrow/impl-flechette/schema.ts +202 -0
  136. package/src/arrow/impl-flechette/statistics.ts +198 -0
  137. package/src/arrow/index.ts +129 -0
  138. package/src/arrow/predicates.ts +66 -0
  139. package/src/arrow/repr.test-d.ts +189 -0
  140. package/src/arrow/schema-types.ts +213 -0
  141. package/src/arrow/types.ts +74 -0
  142. package/src/catalog/attach-option.ts +107 -0
  143. package/src/catalog/attach-options.ts +79 -0
  144. package/src/catalog/composite.ts +180 -0
  145. package/src/catalog/descriptors.ts +164 -0
  146. package/src/catalog/interface.ts +721 -0
  147. package/src/catalog/read-only.ts +909 -0
  148. package/src/client/client.ts +1348 -0
  149. package/src/client/errors.ts +69 -0
  150. package/src/client/helpers.ts +56 -0
  151. package/src/client/protocol.ts +33 -0
  152. package/src/client/types.ts +140 -0
  153. package/src/client-entry.ts +112 -0
  154. package/src/codec/asd.ts +147 -0
  155. package/src/crypto.ts +143 -0
  156. package/src/errors.ts +61 -0
  157. package/src/filter-pushdown/collector.ts +147 -0
  158. package/src/filter-pushdown/deserialize.ts +253 -0
  159. package/src/filter-pushdown/evaluate.ts +591 -0
  160. package/src/filter-pushdown/index.ts +28 -0
  161. package/src/filter-pushdown/types.ts +91 -0
  162. package/src/functions/aggregate.ts +384 -0
  163. package/src/functions/copy-from.ts +282 -0
  164. package/src/functions/copy-to.ts +319 -0
  165. package/src/functions/registry.ts +258 -0
  166. package/src/functions/scalar.ts +356 -0
  167. package/src/functions/storage-cf-do.ts +303 -0
  168. package/src/functions/storage.ts +521 -0
  169. package/src/functions/table-buffering.ts +562 -0
  170. package/src/functions/table-in-out.ts +373 -0
  171. package/src/functions/table.ts +521 -0
  172. package/src/functions/types.ts +192 -0
  173. package/src/generated/vgi-client.ts +560 -0
  174. package/src/generated/vgi-protocol-schemas.ts +864 -0
  175. package/src/index.core.ts +267 -0
  176. package/src/index.ts +11 -0
  177. package/src/metadata/resolve.ts +109 -0
  178. package/src/metadata/serialize.ts +242 -0
  179. package/src/metadata/types.ts +60 -0
  180. package/src/protocol/dispatch.ts +52 -0
  181. package/src/protocol/handlers/aggregate.ts +355 -0
  182. package/src/protocol/handlers/catalog/admin.ts +368 -0
  183. package/src/protocol/handlers/catalog/index.ts +25 -0
  184. package/src/protocol/handlers/catalog/index_methods.ts +65 -0
  185. package/src/protocol/handlers/catalog/macro.ts +114 -0
  186. package/src/protocol/handlers/catalog/shared.ts +291 -0
  187. package/src/protocol/handlers/catalog/table.ts +401 -0
  188. package/src/protocol/handlers/catalog/view.ts +119 -0
  189. package/src/protocol/handlers/function.ts +302 -0
  190. package/src/protocol/handlers/shared.ts +80 -0
  191. package/src/protocol/handlers/table-buffering.ts +207 -0
  192. package/src/protocol/serialize.ts +23 -0
  193. package/src/protocol/serializers/arguments.ts +160 -0
  194. package/src/protocol/serializers/bind.ts +212 -0
  195. package/src/protocol/serializers/cardinality.ts +45 -0
  196. package/src/protocol/serializers/init.ts +247 -0
  197. package/src/protocol/serializers/shared.ts +46 -0
  198. package/src/protocol/state-serializer.ts +181 -0
  199. package/src/protocol/types.ts +153 -0
  200. package/src/secrets/helpers.ts +70 -0
  201. package/src/types.ts +71 -0
  202. package/src/util/arrow/index.ts +23 -0
  203. package/src/util/bytes.ts +14 -0
  204. package/src/util/statistics.ts +40 -0
  205. package/src/worker-cf-entry.ts +172 -0
  206. package/src/worker.ts +212 -0
@@ -0,0 +1,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
+ }