@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,909 @@
1
+ // Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+ // ReadOnlyCatalogInterface: derives catalog from registered functions + descriptors.
3
+
4
+ import { type VgiSchema, schema as schema_, type VgiField, field, type VgiDataType, int64, utf8, binary, bool, nullType, list, isInt } from "../arrow/index.js";
5
+ import {
6
+ CatalogInterface,
7
+ type AttachOpaqueData,
8
+ type TransactionOpaqueData,
9
+ type CatalogAttachResult,
10
+ type SchemaInfo,
11
+ type CatalogInfo,
12
+ type TableInfo,
13
+ type ViewInfo,
14
+ type FunctionInfo,
15
+ type MacroInfo,
16
+ type IndexInfo,
17
+ type IndexConstraintType,
18
+ type CopyFromFormatInfo,
19
+ } from "./interface.js";
20
+ import type { CatalogDescriptor, SchemaDescriptor, TableDescriptor, ViewDescriptor, MacroDescriptor, SettingDescriptor, SecretTypeDescriptor, ForeignKeyDef, DefaultValue } from "./descriptors.js";
21
+ import { serializeColumnStatistics } from "../util/statistics.js";
22
+ import type { VgiFunction } from "../functions/types.js";
23
+ import type { FunctionRegistry } from "../functions/registry.js";
24
+ import { argumentSpecsToSchema, macroArgumentsSchema } from "../arguments/argument-spec.js";
25
+ import { serializeSchema, serializeBatch, emptyBatch, batchFromColumns } from "../util/arrow/index.js";
26
+ import { resolveMetadata } from "../metadata/resolve.js";
27
+ import { FunctionStability, NullHandling, OrderPreservation, DEFAULT_MAX_WORKERS } from "../types.js";
28
+ import { Arguments } from "../arguments/arguments.js";
29
+
30
+ export class ReadOnlyCatalogInterface extends CatalogInterface {
31
+ private _descriptor: CatalogDescriptor;
32
+ private _registry: FunctionRegistry;
33
+ private _attachments = new Map<string, AttachOpaqueData>();
34
+ private _version = 1;
35
+
36
+ constructor(descriptor: CatalogDescriptor, registry: FunctionRegistry) {
37
+ super();
38
+ this._descriptor = descriptor;
39
+ this._registry = registry;
40
+ }
41
+
42
+ catalogs(): string[] {
43
+ return [this._descriptor.name];
44
+ }
45
+
46
+ // Advertise the catalog's discovery record. The declarative path carries no
47
+ // version metadata or attach option specs, but it does surface the optional
48
+ // `source_url` from the descriptor so it flows through `catalog_catalogs`
49
+ // (DuckDB's `duckdb_databases()`).
50
+ override catalogsInfo(): CatalogInfo[] {
51
+ return [{
52
+ name: this._descriptor.name,
53
+ implementation_version: null,
54
+ data_version_spec: null,
55
+ attach_option_specs: [],
56
+ releases: [],
57
+ source_url: this._descriptor.sourceUrl ?? null,
58
+ }];
59
+ }
60
+
61
+ attach(
62
+ name: string,
63
+ options?: Record<string, unknown>,
64
+ dataVersionSpec?: string | null,
65
+ implementationVersion?: string | null,
66
+ ): CatalogAttachResult | Promise<CatalogAttachResult> {
67
+ if (!this.catalogs().includes(name)) {
68
+ throw new Error(`No worker handles catalog '${name}'`);
69
+ }
70
+
71
+ const attachOpaqueData = new Uint8Array(16);
72
+ crypto.getRandomValues(attachOpaqueData);
73
+ this._attachments.set(name, attachOpaqueData);
74
+
75
+ const settings = (this._descriptor.settings ?? []).map((s) =>
76
+ serializeSetting(s)
77
+ );
78
+
79
+ const secretTypes = (this._descriptor.secretTypes ?? []).map((s) =>
80
+ serializeSecretType(s)
81
+ );
82
+
83
+ // Auto-derive supportsTimeTravel from table descriptors
84
+ const hasTimeTravel = this._descriptor.schemas.some(
85
+ (s) => s.tables?.some((t) => t.supportsTimeTravel) ?? false
86
+ );
87
+
88
+ return {
89
+ attach_opaque_data: attachOpaqueData,
90
+ supports_transactions: false,
91
+ supports_time_travel: hasTimeTravel,
92
+ catalog_version_frozen: true,
93
+ catalog_version: this._version,
94
+ attach_opaque_data_required: true,
95
+ default_schema: this._descriptor.defaultSchema ?? "main",
96
+ settings,
97
+ secret_types: secretTypes,
98
+ comment: this._descriptor.comment ?? null,
99
+ tags: this._descriptor.tags ?? {},
100
+ // Advertise true so DuckDB routes catalog_table_column_statistics_get
101
+ // for tables whose TableInfo.supports_column_statistics is also true.
102
+ supports_column_statistics: true,
103
+ resolved_data_version: null,
104
+ resolved_implementation_version: null,
105
+ };
106
+ }
107
+
108
+ detach(attachOpaqueData: AttachOpaqueData): void {
109
+ // Remove attachment
110
+ for (const [name, id] of this._attachments) {
111
+ if (bufferEquals(id, attachOpaqueData)) {
112
+ this._attachments.delete(name);
113
+ break;
114
+ }
115
+ }
116
+ }
117
+
118
+ version(attachOpaqueData: AttachOpaqueData, transactionOpaqueData?: TransactionOpaqueData): number | Promise<number> {
119
+ return this._version;
120
+ }
121
+
122
+ schemas(attachOpaqueData: AttachOpaqueData, transactionOpaqueData?: TransactionOpaqueData): SchemaInfo[] {
123
+ return this._descriptor.schemas.map((s) => ({
124
+ attach_opaque_data: attachOpaqueData,
125
+ name: s.name,
126
+ comment: s.comment ?? null,
127
+ tags: s.tags ?? {},
128
+ estimated_object_count: this._estimatedObjectCount(s),
129
+ }));
130
+ }
131
+
132
+ // Compute per-kind object counts so the C++ extension's eager-load gate
133
+ // can skip bulk RPCs for empty kinds. Zero is load-bearing — absence
134
+ // would read as "unknown=1" and suppress the bypass. Function counts are
135
+ // partitioned by kind to match python's behavior.
136
+ private _estimatedObjectCount(s: SchemaDescriptor): Record<string, number> {
137
+ let scalar = 0, aggregate = 0, table = 0;
138
+ for (const f of s.functions ?? []) {
139
+ if (f.kind === "scalar") scalar++;
140
+ else if ((f.kind as string) === "aggregate") aggregate++;
141
+ else table++; // "table" and "table_in_out"
142
+ }
143
+ return {
144
+ table: s.tables?.length ?? 0,
145
+ view: s.views?.length ?? 0,
146
+ scalar_function: scalar,
147
+ aggregate_function: aggregate,
148
+ table_function: table,
149
+ macro: s.macros?.length ?? 0,
150
+ index: s.indexes?.length ?? 0,
151
+ };
152
+ }
153
+
154
+ override schemaGet(
155
+ attachOpaqueData: AttachOpaqueData,
156
+ name: string,
157
+ transactionOpaqueData?: TransactionOpaqueData
158
+ ): SchemaInfo | null {
159
+ const desc = this._descriptor.schemas.find((s) => s.name === name);
160
+ if (!desc) return null;
161
+ return {
162
+ attach_opaque_data: attachOpaqueData,
163
+ name: desc.name,
164
+ comment: desc.comment ?? null,
165
+ tags: desc.tags ?? {},
166
+ estimated_object_count: this._estimatedObjectCount(desc),
167
+ };
168
+ }
169
+
170
+ override async schemaContentsTables(
171
+ attachOpaqueData: AttachOpaqueData,
172
+ name: string,
173
+ transactionOpaqueData?: TransactionOpaqueData
174
+ ): Promise<TableInfo[]> {
175
+ const schema = this._descriptor.schemas.find((s) => s.name === name);
176
+ if (!schema || !schema.tables) return [];
177
+
178
+ return await Promise.all(schema.tables.map(async (t) => {
179
+ // Resolve the column schema
180
+ let colSchema: VgiSchema;
181
+ if (t.columns) {
182
+ colSchema = t.columns;
183
+ } else if (t.function) {
184
+ const func = t.function;
185
+ const dummyArgs = t.arguments ?? new Arguments();
186
+ try {
187
+ const bindRequest = {
188
+ function_name: func.meta.name,
189
+ arguments: dummyArgs,
190
+ function_type: "table" as any,
191
+ input_schema: null,
192
+ settings: null,
193
+ secrets: null,
194
+ attach_opaque_data: attachOpaqueData,
195
+ transaction_opaque_data: null,
196
+ resolved_secrets_provided: false,
197
+ };
198
+ const response = await func.bind(bindRequest);
199
+ colSchema = response.output_schema;
200
+ } catch {
201
+ colSchema = schema_([]);
202
+ }
203
+ } else {
204
+ colSchema = schema_([]);
205
+ }
206
+
207
+ // Apply defaults as Arrow field metadata
208
+ if (t.defaults) {
209
+ colSchema = applyDefaults(colSchema, t.defaults);
210
+ }
211
+ // Apply generated-column expressions as Arrow field metadata
212
+ // (`generated_expression`). DuckDB registers these columns as GENERATED
213
+ // and evaluates the SQL expression client-side — the scan function
214
+ // should not return values for them.
215
+ if (t.generatedColumns) {
216
+ colSchema = applyGeneratedColumns(colSchema, t.generatedColumns);
217
+ }
218
+ // Apply per-column comments as Arrow field metadata (matches Python's
219
+ // `comment` metadata key on columns).
220
+ if (t.columnComments) {
221
+ colSchema = applyColumnComments(colSchema, t.columnComments);
222
+ }
223
+
224
+ const columns = serializeSchema(colSchema);
225
+
226
+ // Resolve constraint column names → indices
227
+ const notNullIndices = resolveIndices(colSchema, t.notNull);
228
+ const uniqueIndices = resolveIndexGroups(colSchema, t.unique);
229
+ const pkIndices = resolveIndexGroups(colSchema, t.primaryKey);
230
+ const fkBytes = serializeForeignKeys(t.foreignKey, name);
231
+
232
+ // Validate required_field_filter_paths: the leading dotted segment of
233
+ // each path must be a real column on this table. Struct subfield
234
+ // validity is not checked here — DuckDB's binder catches typos at scan
235
+ // time, and the descriptor doesn't unpack STRUCT subfields. (Mirrors
236
+ // vgi-python's Table descriptor validation.)
237
+ const requiredFieldFilterPaths = t.requiredFieldFilterPaths ?? [];
238
+ if (requiredFieldFilterPaths.length > 0) {
239
+ const columnNames = new Set(colSchema.fields.map((f) => f.name));
240
+ for (const path of requiredFieldFilterPaths) {
241
+ if (!path) {
242
+ throw new Error(
243
+ `Table '${t.name}': requiredFieldFilterPaths must not contain empty strings`
244
+ );
245
+ }
246
+ const head = path.split(".", 1)[0];
247
+ if (!columnNames.has(head)) {
248
+ throw new Error(
249
+ `Table '${t.name}': requiredFieldFilterPaths path '${path}' references ` +
250
+ `unknown column '${head}'. Available columns: ${[...columnNames].sort().join(", ")}`
251
+ );
252
+ }
253
+ }
254
+ }
255
+
256
+ return {
257
+ comment: t.comment ?? null,
258
+ tags: t.tags ?? {},
259
+ name: t.name,
260
+ schema_name: name,
261
+ columns,
262
+ not_null_constraints: notNullIndices,
263
+ unique_constraints: uniqueIndices,
264
+ check_constraints: t.check ?? [],
265
+ primary_key_constraints: pkIndices,
266
+ foreign_key_constraints: fkBytes,
267
+ supports_insert: false,
268
+ supports_update: false,
269
+ supports_delete: false,
270
+ supports_returning: false,
271
+ supports_column_statistics: t.statistics != null && Object.keys(t.statistics).length > 0,
272
+ // Inline scan_function only for purely function-backed tables.
273
+ // When explicit `columns` are also declared, the table is on the
274
+ // legacy catalog_table_scan_function_get path; inlining would
275
+ // shadow the per-call dispatch and prevent workers from varying
276
+ // the scan function (e.g. version-resolved time-travel).
277
+ scan_function: t.function && !t.columns
278
+ ? inlineScanFunction(t.function, t.arguments)
279
+ : null,
280
+ insert_function: null,
281
+ update_function: null,
282
+ delete_function: null,
283
+ cardinality_estimate: t.inlinedCardinality
284
+ ? Number(t.inlinedCardinality.estimate)
285
+ : null,
286
+ cardinality_max: t.inlinedCardinality
287
+ ? Number(t.inlinedCardinality.max)
288
+ : null,
289
+ required_field_filter_paths: requiredFieldFilterPaths,
290
+ };
291
+ }));
292
+ }
293
+
294
+ override tableColumnStatisticsGet(
295
+ attachOpaqueData: AttachOpaqueData,
296
+ schemaName: string,
297
+ name: string,
298
+ transactionOpaqueData?: TransactionOpaqueData,
299
+ ): { bytes: Uint8Array; cacheMaxAgeSeconds: number | null } | null {
300
+ const schema = this._descriptor.schemas.find((s) => s.name === schemaName);
301
+ if (!schema || !schema.tables) return null;
302
+ const table = schema.tables.find((t) => t.name === name);
303
+ if (!table || !table.statistics) return null;
304
+ const stats = Object.values(table.statistics);
305
+ if (stats.length === 0) return null;
306
+ return {
307
+ bytes: serializeColumnStatistics(stats),
308
+ cacheMaxAgeSeconds: table.statisticsCacheMaxAgeSeconds ?? null,
309
+ };
310
+ }
311
+
312
+ override schemaContentsViews(
313
+ attachOpaqueData: AttachOpaqueData,
314
+ name: string,
315
+ transactionOpaqueData?: TransactionOpaqueData
316
+ ): ViewInfo[] {
317
+ const schema = this._descriptor.schemas.find((s) => s.name === name);
318
+ if (!schema || !schema.views) return [];
319
+
320
+ return schema.views.map((v) => ({
321
+ comment: v.comment ?? null,
322
+ tags: v.tags ?? {},
323
+ name: v.name,
324
+ schema_name: name,
325
+ definition: v.definition,
326
+ column_comments: v.columnComments ?? {},
327
+ }));
328
+ }
329
+
330
+ override schemaContentsFunctions(
331
+ attachOpaqueData: AttachOpaqueData,
332
+ name: string,
333
+ type: string,
334
+ transactionOpaqueData?: TransactionOpaqueData
335
+ ): FunctionInfo[] {
336
+ const schema = this._descriptor.schemas.find((s) => s.name === name);
337
+ if (!schema || !schema.functions) return [];
338
+
339
+ return schema.functions
340
+ .filter((f) => {
341
+ // Filter by SchemaObjectType (lowercase on the wire: scalar_function,
342
+ // table_function, aggregate_function). Unknown filter values pass
343
+ // everything through. Defensive String() coerces null/undefined.
344
+ const t = String(type ?? "").toUpperCase();
345
+ if (t === "SCALAR_FUNCTION") return f.kind === "scalar";
346
+ if (t === "TABLE_FUNCTION") return f.kind === "table" || f.kind === "table_in_out" || (f.kind as string) === "table_buffering";
347
+ if (t === "AGGREGATE_FUNCTION") return (f.kind as string) === "aggregate";
348
+ return true;
349
+ })
350
+ .map((f) => {
351
+ const meta = resolveMetadata(f);
352
+ const argSchema = argumentSpecsToSchema(f.argumentSpecs);
353
+ const argBytes = serializeSchema(argSchema);
354
+
355
+ // Use the function's default output schema if available,
356
+ // otherwise empty schema (table functions determine output at bind time)
357
+ const outputSchemaBytes = serializeSchema(
358
+ f.defaultOutputSchema ?? schema_([])
359
+ );
360
+
361
+ const requiredSecrets = (meta.requiredSecrets ?? []).map(
362
+ (secretType: string) => ({ secret_type: secretType, scope: null, secret_name: null })
363
+ );
364
+
365
+ // Function-level tags surfaced into duckdb_functions().tags. The wire
366
+ // map is string→string, so the metadata linter (vgi-lint-check) and any
367
+ // worker-side conventions (e.g. VGI307's `vgi.columns_md` on a dynamic
368
+ // table function) can read them. Worker-provided tags are MERGED OVER
369
+ // any SDK-derived tags rather than replacing them — and derived tags
370
+ // never clobber worker-set keys. There are no SDK-derived function tags
371
+ // today (examples flow into FunctionInfo.examples, not a tag), so the
372
+ // derived base is empty; the merge keeps that invariant if one is ever
373
+ // added.
374
+ const derivedTags: Record<string, string> = {};
375
+ const tags: Record<string, string> = { ...derivedTags, ...(meta.tags ?? {}) };
376
+
377
+ return {
378
+ comment: null,
379
+ tags,
380
+ name: f.meta.name,
381
+ schema_name: name,
382
+ function_type: meta.functionType.toUpperCase() as "SCALAR" | "TABLE" | "TABLE_BUFFERING" | "AGGREGATE",
383
+ arguments: argBytes,
384
+ output_schema: outputSchemaBytes,
385
+ stability: meta.stability as any,
386
+ null_handling: meta.nullHandling as any,
387
+ description: meta.description,
388
+ examples: meta.examples.map((e) => ({
389
+ sql: e.sql,
390
+ description: e.description,
391
+ expected_output: null,
392
+ })),
393
+ categories: meta.categories,
394
+ projection_pushdown: meta.projectionPushdown,
395
+ filter_pushdown: meta.filterPushdown,
396
+ sampling_pushdown: meta.samplingPushdown,
397
+ // Mirror Python: None for scalars, the meta flag for table functions.
398
+ // The C++ extension only honours this for rowid-bearing tables.
399
+ late_materialization:
400
+ (f.kind as string) === "table" && f.meta.lateMaterialization === true ? true : null,
401
+ supported_expression_filters: meta.supportedExpressionFilters,
402
+ order_preservation: meta.preservesOrder as any,
403
+ max_workers: meta.maxWorkers ?? DEFAULT_MAX_WORKERS,
404
+ // Opt-in features the read-only catalog's functions do not use; the
405
+ // regenerated FunctionInfo schema requires these non-nullable
406
+ // fields, so default them explicitly. table_buffering functions
407
+ // thread their batch_index requirement through supports_batch_index
408
+ // so the C++ operator declares RequiredPartitionInfo=BatchIndex.
409
+ supports_batch_index:
410
+ ((f.kind as string) === "table_buffering" && f.meta.requiresInputBatchIndex === true) ||
411
+ ((f.kind as string) === "table" && f.meta.supportsBatchIndex === true),
412
+ partition_kind:
413
+ (f.kind as string) === "table" && f.meta.partitionKind
414
+ ? f.meta.partitionKind
415
+ : "NOT_PARTITIONED",
416
+ order_dependent: meta.orderDependent as any,
417
+ distinct_dependent: meta.distinctDependent as any,
418
+ supports_window: false,
419
+ streaming_partitioned: false,
420
+ // table_in_out functions with a finalize callback, AND every
421
+ // table_buffering function (the Source phase always runs), get the
422
+ // FINALIZE init() phase. Advertising has_finalize=true on a function
423
+ // without it makes DuckDB call FinalExecute and crash with
424
+ // 'FinalExecute not supported for project_input'.
425
+ has_finalize:
426
+ (f.kind === "table_in_out" && f.meta.hasFinalize === true) ||
427
+ (f.kind as string) === "table_buffering",
428
+ source_order_dependent:
429
+ (f.kind as string) === "table_buffering" && f.meta.sourceOrderDependent === true,
430
+ sink_order_dependent:
431
+ (f.kind as string) === "table_buffering" && f.meta.sinkOrderDependent === true,
432
+ requires_input_batch_index:
433
+ (f.kind as string) === "table_buffering" && f.meta.requiresInputBatchIndex === true,
434
+ required_settings: meta.requiredSettings,
435
+ required_secrets: requiredSecrets.map((s) => ({
436
+ secret_type: s.secret_type,
437
+ scope: s.scope,
438
+ secret_name: s.secret_name,
439
+ })),
440
+ };
441
+ });
442
+ }
443
+
444
+ override schemaContentsMacros(
445
+ attachOpaqueData: AttachOpaqueData,
446
+ name: string,
447
+ type: string,
448
+ transactionOpaqueData?: TransactionOpaqueData
449
+ ): MacroInfo[] {
450
+ const schema = this._descriptor.schemas.find((s) => s.name === name);
451
+ if (!schema || !schema.macros) return [];
452
+
453
+ return schema.macros
454
+ .filter((m) => {
455
+ // Defensive String() — see schemaContentsFunctions for rationale.
456
+ const t = String(type ?? "").toLowerCase();
457
+ if (t === "scalar_macro" && m.macroType !== "scalar") return false;
458
+ if (t === "table_macro" && m.macroType !== "table") return false;
459
+ return true;
460
+ })
461
+ .map((m) => ({
462
+ comment: m.comment ?? null,
463
+ tags: m.tags ?? {},
464
+ name: m.name,
465
+ schema_name: name,
466
+ macro_type: (m.macroType === "scalar" ? "SCALAR" : "TABLE") as "SCALAR" | "TABLE",
467
+ parameters: m.parameters,
468
+ parameter_default_values: m.parameterDefaultValues ?? new Uint8Array(0),
469
+ definition: m.definition,
470
+ // One nullable Arrow field per parameter (in order), carrying each
471
+ // documented parameter's description via the vgi_doc field-metadata key
472
+ // (the same channel functions use). Type = default-value type when known
473
+ // else null. Serialized as IPC bytes into the generated schema slot.
474
+ arguments_schema: serializeSchema(
475
+ macroArgumentsSchema(m.parameters, m.parameterDefaultValues, m.parameterDocs),
476
+ ),
477
+ }));
478
+ }
479
+
480
+ override schemaContentsIndexes(
481
+ attachOpaqueData: AttachOpaqueData,
482
+ name: string,
483
+ transactionOpaqueData?: TransactionOpaqueData
484
+ ): IndexInfo[] {
485
+ const schema = this._descriptor.schemas.find((s) => s.name === name);
486
+ if (!schema || !schema.indexes) return [];
487
+ return schema.indexes.map((idx) => ({
488
+ comment: idx.comment ?? null,
489
+ tags: idx.tags ?? {},
490
+ name: idx.name,
491
+ schema_name: name,
492
+ table_name: idx.tableName,
493
+ index_type: idx.indexType ?? "ART",
494
+ constraint_type: (idx.constraintType ?? "NONE") as IndexConstraintType,
495
+ expressions: idx.expressions,
496
+ options: idx.options ?? {},
497
+ }));
498
+ }
499
+
500
+ override indexGet(
501
+ attachOpaqueData: AttachOpaqueData,
502
+ schemaName: string,
503
+ name: string,
504
+ transactionOpaqueData?: TransactionOpaqueData
505
+ ): IndexInfo | null {
506
+ const all = this.schemaContentsIndexes(attachOpaqueData, schemaName, transactionOpaqueData);
507
+ return all.find((i) => i.name === name) ?? null;
508
+ }
509
+
510
+ override macroGet(
511
+ attachOpaqueData: AttachOpaqueData,
512
+ schemaName: string,
513
+ name: string,
514
+ transactionOpaqueData?: TransactionOpaqueData
515
+ ): MacroInfo | null {
516
+ const macros = this.schemaContentsMacros(attachOpaqueData, schemaName, "SCALAR_MACRO", transactionOpaqueData);
517
+ const tableMacros = this.schemaContentsMacros(attachOpaqueData, schemaName, "TABLE_MACRO", transactionOpaqueData);
518
+ const all = [...macros, ...tableMacros];
519
+ return all.find((m) => m.name.toLowerCase() === name.toLowerCase()) ?? null;
520
+ }
521
+
522
+ override tableScanFunctionGet(
523
+ attachOpaqueData: AttachOpaqueData,
524
+ schemaName: string,
525
+ name: string,
526
+ atUnit?: string,
527
+ atValue?: string,
528
+ transactionOpaqueData?: TransactionOpaqueData
529
+ ): any {
530
+ validateAtParams(atUnit, atValue);
531
+
532
+ // Find the table descriptor
533
+ const schema = this._descriptor.schemas.find((s) => s.name === schemaName);
534
+ if (!schema || !schema.tables) {
535
+ throw new Error(`Table '${name}' not found in schema '${schemaName}'`);
536
+ }
537
+ const table = schema.tables.find((t) => t.name === name);
538
+ if (!table) {
539
+ throw new Error(`Table '${name}' not found in schema '${schemaName}'`);
540
+ }
541
+
542
+ // Reject AT clause on tables that don't support time travel
543
+ if (atUnit && !table.supportsTimeTravel) {
544
+ throw new Error(`Table '${schemaName}.${name}' does not support time travel queries`);
545
+ }
546
+
547
+ if (table.function) {
548
+ // Function-backed table
549
+ const args = table.arguments ?? new Arguments();
550
+ const argSchema = argumentSpecsToSchema(table.function.argumentSpecs);
551
+ const argBytes = serializeArgsBatch(args, argSchema);
552
+ return {
553
+ function_name: table.function.meta.name,
554
+ arguments: argBytes,
555
+ required_extensions: [],
556
+ };
557
+ }
558
+
559
+ throw new Error(`Table '${name}' is not function-backed`);
560
+ }
561
+
562
+ override async tableGet(
563
+ attachOpaqueData: AttachOpaqueData,
564
+ schemaName: string,
565
+ name: string,
566
+ atUnit?: string,
567
+ atValue?: string,
568
+ transactionOpaqueData?: TransactionOpaqueData
569
+ ): Promise<TableInfo | null> {
570
+ validateAtParams(atUnit, atValue);
571
+
572
+ // Find the table descriptor to check supports_time_travel
573
+ const schema = this._descriptor.schemas.find((s) => s.name === schemaName);
574
+ if (schema?.tables) {
575
+ const tableDesc = schema.tables.find((t) => t.name === name);
576
+ if (tableDesc && atUnit && !tableDesc.supportsTimeTravel) {
577
+ throw new Error(`Table '${schemaName}.${name}' does not support time travel queries`);
578
+ }
579
+ }
580
+
581
+ const tables = await this.schemaContentsTables(attachOpaqueData, schemaName, transactionOpaqueData);
582
+ return tables.find((t) => t.name === name) ?? null;
583
+ }
584
+
585
+ override viewGet(
586
+ attachOpaqueData: AttachOpaqueData,
587
+ schemaName: string,
588
+ name: string,
589
+ transactionOpaqueData?: TransactionOpaqueData
590
+ ): ViewInfo | null {
591
+ const views = this.schemaContentsViews(attachOpaqueData, schemaName, transactionOpaqueData);
592
+ return views.find((v) => v.name === name) ?? null;
593
+ }
594
+
595
+ // Advertise every COPY-FROM format reader registered in this catalog. Walks
596
+ // the descriptor's functions for those carrying `meta.copyFromFormat` (set by
597
+ // defineCopyFromFunction) and converts each into a CopyFromFormatInfo. The
598
+ // option schema reuses the same argument serialization as functions, so
599
+ // option types / defaults / docs surface identically to
600
+ // vgi_function_arguments(). Mirrors vgi-python's
601
+ // ReadOnlyCatalogInterface.copy_from_formats.
602
+ override copyFromFormats(
603
+ attachOpaqueData: AttachOpaqueData,
604
+ transactionOpaqueData?: TransactionOpaqueData,
605
+ ): CopyFromFormatInfo[] {
606
+ const formats: CopyFromFormatInfo[] = [];
607
+ // Key by (direction, format): a FROM-format and a TO-format may legitimately
608
+ // share a name; the extension scopes by attach alias anyway. Mirrors
609
+ // vgi-python's `dedup_key = f"{direction}:{fmt}"`.
610
+ const seen = new Set<string>();
611
+ for (const schema of this._descriptor.schemas) {
612
+ for (const f of schema.functions ?? []) {
613
+ // A format is a COPY FROM reader (copyFromFormat) or a COPY TO writer
614
+ // (copyToFormat). The RPC name is historical; it returns all directions.
615
+ const isTo = !!f.meta.copyToFormat;
616
+ const fmt = isTo ? f.meta.copyToFormat : f.meta.copyFromFormat;
617
+ if (!fmt) continue;
618
+ const direction = isTo
619
+ ? f.meta.copyToDirection ?? "to"
620
+ : f.meta.copyFromDirection ?? "from";
621
+ const dedupKey = `${direction}:${fmt}`;
622
+ if (seen.has(dedupKey)) continue;
623
+ seen.add(dedupKey);
624
+ const meta = resolveMetadata(f);
625
+ // Only the declared options are part of the format's option list — the
626
+ // synthetic table-input ("data") spec a TO writer carries as a
627
+ // table_buffering function is not an option (mirrors vgi-python, whose
628
+ // CopyToFunction options are just the Arg-annotated arguments).
629
+ const optionSpecs = f.argumentSpecs.filter((s) => !s.isTableInput);
630
+ const argSchema = argumentSpecsToSchema(optionSpecs);
631
+ // ordered only meaningful for TO writers (single-thread sink ingest).
632
+ const ordered = isTo && f.meta.sinkOrderDependent === true;
633
+ formats.push({
634
+ comment: (isTo ? f.meta.copyToComment : f.meta.copyFromComment) ?? null,
635
+ tags: meta.tags,
636
+ format_name: fmt,
637
+ handler: meta.name,
638
+ options: serializeSchema(argSchema),
639
+ direction,
640
+ description: meta.description ?? "",
641
+ ordered,
642
+ });
643
+ }
644
+ }
645
+ return formats;
646
+ }
647
+ }
648
+
649
+ // ============================================================================
650
+ // Constraint resolution helpers
651
+ // ============================================================================
652
+
653
+ function resolveIndices(schema: VgiSchema, names?: string[]): number[] {
654
+ if (!names || names.length === 0) return [];
655
+ return names.map((n) => {
656
+ const idx = schema.fields.findIndex((f) => f.name === n);
657
+ if (idx < 0) throw new Error(`Column '${n}' not found in schema`);
658
+ return idx;
659
+ });
660
+ }
661
+
662
+ function resolveIndexGroups(schema: VgiSchema, groups?: string[][]): number[][] {
663
+ if (!groups || groups.length === 0) return [];
664
+ return groups.map((group) => resolveIndices(schema, group));
665
+ }
666
+
667
+ const FK_SCHEMA = schema_([
668
+ field("fk_columns", list(field("item", utf8(), true)), false),
669
+ field("pk_columns", list(field("item", utf8(), true)), false),
670
+ field("referenced_table", utf8(), false),
671
+ field("referenced_schema", utf8(), false),
672
+ ]);
673
+
674
+ function serializeForeignKeys(fks?: ForeignKeyDef[], schemaName?: string): Uint8Array[] {
675
+ if (!fks || fks.length === 0) return [];
676
+ return fks.map((fk) =>
677
+ serializeBatch(
678
+ batchFromColumns(
679
+ {
680
+ fk_columns: [fk.columns],
681
+ pk_columns: [fk.referencedColumns],
682
+ referenced_table: [fk.referencedTable],
683
+ referenced_schema: [fk.referencedSchema ?? schemaName ?? "main"],
684
+ },
685
+ FK_SCHEMA
686
+ )
687
+ )
688
+ );
689
+ }
690
+
691
+ function defaultToSql(value: DefaultValue): string {
692
+ if (value === null || value === undefined) return "NULL";
693
+ if (typeof value === "boolean") return value ? "true" : "false";
694
+ if (typeof value === "number") return String(value);
695
+ // string — quote as SQL string literal
696
+ const escaped = value.replace(/'/g, "''");
697
+ return `'${escaped}'`;
698
+ }
699
+
700
+ function applyDefaults(schema: VgiSchema, defaults: Record<string, DefaultValue>): VgiSchema {
701
+ let result = schema;
702
+ for (const [colName, value] of Object.entries(defaults)) {
703
+ const idx = result.fields.findIndex((f) => f.name === colName);
704
+ if (idx < 0) continue;
705
+ const f = result.fields[idx];
706
+ const existingMeta = f.metadata ? new Map(f.metadata) : new Map<string, string>();
707
+ existingMeta.set("default", defaultToSql(value));
708
+ const newField = field(f.name, f.type, f.nullable, existingMeta);
709
+ const fields = [...result.fields];
710
+ fields[idx] = newField;
711
+ result = schema_(fields);
712
+ }
713
+ return result;
714
+ }
715
+
716
+ function applyGeneratedColumns(schema: VgiSchema, generated: Record<string, string>): VgiSchema {
717
+ let result = schema;
718
+ for (const [colName, expression] of Object.entries(generated)) {
719
+ const idx = result.fields.findIndex((f) => f.name === colName);
720
+ if (idx < 0) continue;
721
+ const f = result.fields[idx];
722
+ const existingMeta = f.metadata ? new Map(f.metadata) : new Map<string, string>();
723
+ existingMeta.set("generated_expression", expression);
724
+ const newField = field(f.name, f.type, f.nullable, existingMeta);
725
+ const fields = [...result.fields];
726
+ fields[idx] = newField;
727
+ result = schema_(fields);
728
+ }
729
+ return result;
730
+ }
731
+
732
+ function applyColumnComments(schema: VgiSchema, comments: Record<string, string>): VgiSchema {
733
+ let result = schema;
734
+ for (const [colName, comment] of Object.entries(comments)) {
735
+ if (!comment) continue;
736
+ const idx = result.fields.findIndex((f) => f.name === colName);
737
+ if (idx < 0) continue;
738
+ const f = result.fields[idx];
739
+ const existingMeta = f.metadata ? new Map(f.metadata) : new Map<string, string>();
740
+ existingMeta.set("comment", comment);
741
+ const newField = field(f.name, f.type, f.nullable, existingMeta);
742
+ const fields = [...result.fields];
743
+ fields[idx] = newField;
744
+ result = schema_(fields);
745
+ }
746
+ return result;
747
+ }
748
+
749
+ // ============================================================================
750
+ // Setting serialization (matches Python SettingSpec.serialize() format)
751
+ // ============================================================================
752
+
753
+ const SETTING_SCHEMA = schema_([
754
+ field("name", utf8(), false),
755
+ field("description", utf8(), false),
756
+ field("type", binary(), false),
757
+ field("default_value", binary(), true),
758
+ ]);
759
+
760
+ function serializeSetting(setting: SettingDescriptor): Uint8Array {
761
+ // Serialize the Arrow type as a schema with a single "value" field
762
+ const typeSchema = schema_([field("value", setting.type, true)]);
763
+ const typeBytes = serializeSchema(typeSchema);
764
+
765
+ // Serialize the default value as a single-row batch, or null
766
+ let defaultBytes: Uint8Array | null = null;
767
+ if (setting.defaultValue != null) {
768
+ let val: any = setting.defaultValue;
769
+ // Coerce JS numbers to BigInt for 64-bit integer Arrow types
770
+ if (isInt(setting.type) && (setting.type as any).bitWidth === 64) {
771
+ if (typeof val === "number") val = BigInt(val);
772
+ }
773
+ // Coerce BigInt to Number for 32-bit integer Arrow types
774
+ if (isInt(setting.type) && (setting.type as any).bitWidth <= 32) {
775
+ if (typeof val === "bigint") val = Number(val);
776
+ }
777
+ defaultBytes = serializeBatch(
778
+ batchFromColumns({ value: [val] }, typeSchema)
779
+ );
780
+ }
781
+
782
+ // Serialize the outer setting spec batch
783
+ return serializeBatch(
784
+ batchFromColumns(
785
+ {
786
+ name: [setting.name],
787
+ description: [setting.description],
788
+ type: [typeBytes],
789
+ default_value: [defaultBytes],
790
+ },
791
+ SETTING_SCHEMA
792
+ )
793
+ );
794
+ }
795
+
796
+ // ============================================================================
797
+ // Secret type serialization (matches Python SecretTypeSpec.serialize() format)
798
+ // ============================================================================
799
+
800
+ const SECRET_TYPE_SCHEMA = schema_([
801
+ field("name", utf8(), false),
802
+ field("description", utf8(), false),
803
+ field("parameters_schema", binary(), false),
804
+ ]);
805
+
806
+ function serializeSecretType(spec: SecretTypeDescriptor): Uint8Array {
807
+ const parametersSchemaBytes = serializeSchema(spec.schema);
808
+
809
+ return serializeBatch(
810
+ batchFromColumns(
811
+ {
812
+ name: [spec.name],
813
+ description: [spec.description],
814
+ parameters_schema: [parametersSchemaBytes],
815
+ },
816
+ SECRET_TYPE_SCHEMA
817
+ )
818
+ );
819
+ }
820
+
821
+ function bufferEquals(a: Uint8Array, b: Uint8Array): boolean {
822
+ if (a.length !== b.length) return false;
823
+ for (let i = 0; i < a.length; i++) {
824
+ if (a[i] !== b[i]) return false;
825
+ }
826
+ return true;
827
+ }
828
+
829
+ function serializeArgsBatch(args: Arguments, argSpecSchema: VgiSchema): Uint8Array {
830
+ // Serialize arguments as a FLAT batch using the argument spec field names.
831
+ // DuckDB's VGI extension reads each column as a named argument and wraps it
832
+ // with "named_" prefix when sending the bind request. The function extractArgs
833
+ // will look up these by name as a fallback.
834
+ const fields: VgiField[] = [];
835
+ const values: Record<string, any[]> = {};
836
+
837
+ for (let i = 0; i < args.positional.length; i++) {
838
+ const val = args.positional[i];
839
+ const specField = argSpecSchema.fields[i];
840
+ const fieldName = specField ? specField.name : `positional_${i}`;
841
+ const fieldType = specField ? specField.type : inferScalarType(val);
842
+ fields.push(field(fieldName, fieldType, true));
843
+ let coerced = val;
844
+ if (isInt(fieldType) && (fieldType as any).bitWidth === 64) {
845
+ coerced = typeof val === "number" ? BigInt(val) : val;
846
+ }
847
+ values[fieldName] = [coerced];
848
+ }
849
+
850
+ for (const [name, val] of args.named) {
851
+ fields.push(field(name, inferScalarType(val), true));
852
+ values[name] = [val];
853
+ }
854
+
855
+ const batchSchema = schema_(fields);
856
+
857
+ if (fields.length === 0) {
858
+ return serializeBatch(emptyBatch(batchSchema));
859
+ }
860
+
861
+ return serializeBatch(batchFromColumns(values, batchSchema));
862
+ }
863
+
864
+ function validateAtParams(atUnit?: string, atValue?: string): void {
865
+ if (Boolean(atUnit) !== Boolean(atValue)) {
866
+ throw new Error("at_unit and at_value must both be provided or both be absent");
867
+ }
868
+ }
869
+
870
+ function inferScalarType(val: any): VgiDataType {
871
+ if (val === null || val === undefined) return nullType();
872
+ if (typeof val === "string") return utf8();
873
+ if (typeof val === "boolean") return bool();
874
+ if (typeof val === "number") return int64();
875
+ if (typeof val === "bigint") return int64();
876
+ if (val instanceof Uint8Array || val instanceof ArrayBuffer) return binary();
877
+ return utf8();
878
+ }
879
+
880
+ // Build the inlined `scan_function` payload for a function-backed table.
881
+ // Mirrors python's _inline_function_result: a serialized one-row batch of
882
+ // ScanFunctionResultSchema (function_name, arguments, required_extensions).
883
+ // The C++ extension uses these bytes verbatim and skips the
884
+ // catalog_table_scan_function_get RPC.
885
+ function inlineScanFunction(
886
+ func: VgiFunction,
887
+ args: Arguments | undefined,
888
+ ): Uint8Array {
889
+ const argSchema = argumentSpecsToSchema(func.argumentSpecs);
890
+ const argBytes = serializeArgsBatch(args ?? new Arguments(), argSchema);
891
+ const scanSchema = schema_([
892
+ field("function_name", utf8(), false),
893
+ field("arguments", binary(), false),
894
+ field(
895
+ "required_extensions",
896
+ list(field("item", utf8(), true)),
897
+ false,
898
+ ),
899
+ ]);
900
+ const batch = batchFromColumns(
901
+ {
902
+ function_name: [func.meta.name],
903
+ arguments: [argBytes],
904
+ required_extensions: [[] as string[]],
905
+ },
906
+ scanSchema,
907
+ );
908
+ return serializeBatch(batch);
909
+ }