@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,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
|
+
}
|