@query-farm/vgi 0.37.1 → 0.39.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/README.md +74 -0
- package/dist/catalog/composite.d.ts +23 -1
- package/dist/catalog/contents-digest.d.ts +25 -0
- package/dist/catalog/interface.d.ts +107 -2
- package/dist/catalog/read-only.d.ts +45 -1
- package/dist/client/client.d.ts +49 -2
- package/dist/client/types.d.ts +56 -1
- package/dist/client-entry.js +239 -11
- package/dist/client-entry.js.map +9 -8
- package/dist/codec/asd.d.ts +8 -0
- package/dist/functions/copy-to.d.ts +1 -1
- package/dist/generated/vgi-client.d.ts +19 -0
- package/dist/generated/vgi-protocol-schemas.d.ts +3 -0
- package/dist/generated/vgi-protocol-types.d.ts +26 -0
- package/dist/http/fetch.d.ts +2 -1
- package/dist/index.core.d.ts +6 -3
- package/dist/index.js +672 -43
- package/dist/index.js.map +21 -17
- package/dist/protocol/handlers/catalog/contents.d.ts +27 -0
- package/dist/rpc-server.d.ts +97 -0
- package/dist/serve-entry.d.ts +2 -1
- package/dist/serve-entry.js +405 -25
- package/dist/serve-entry.js.map +18 -14
- package/dist/worker-cf-entry.d.ts +3 -0
- package/dist/worker-cf-entry.js +528 -31
- package/dist/worker-cf-entry.js.map +20 -16
- package/dist/worker.d.ts +9 -1
- package/package.json +3 -3
- package/src/catalog/composite.ts +81 -0
- package/src/catalog/contents-digest.ts +78 -0
- package/src/catalog/interface.ts +167 -5
- package/src/catalog/read-only.ts +68 -6
- package/src/client/client.ts +201 -0
- package/src/client/types.ts +58 -1
- package/src/codec/asd.ts +108 -9
- package/src/functions/copy-to.ts +1 -1
- package/src/generated/vgi-client.ts +22 -1
- package/src/generated/vgi-protocol-schemas.ts +30 -1
- package/src/generated/vgi-protocol-types.ts +126 -0
- package/src/http/fetch.ts +10 -2
- package/src/index.core.ts +23 -2
- package/src/protocol/dispatch.ts +1 -1
- package/src/protocol/handlers/catalog/admin.ts +35 -26
- package/src/protocol/handlers/catalog/contents.ts +162 -0
- package/src/protocol/handlers/catalog/macro.ts +2 -1
- package/src/protocol/handlers/catalog/shared.ts +12 -1
- package/src/rpc-server.ts +273 -0
- package/src/serve-entry.ts +7 -1
- package/src/worker-cf-entry.ts +8 -0
- package/src/worker.ts +24 -6
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// GENERATED by vgi.codegen.ts_types. DO NOT EDIT BY HAND.
|
|
3
|
+
//
|
|
4
|
+
// Generator: python -m vgi.codegen.ts_types v1
|
|
5
|
+
// Content hash: d240510f013b
|
|
6
|
+
//
|
|
7
|
+
// To regenerate:
|
|
8
|
+
// uv run --project ~/Development/vgi-python python scripts/regen_generated.py
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
import { encodeASD, decodeASD } from "../codec/asd.js";
|
|
12
|
+
import {
|
|
13
|
+
CatalogAttachResultSchema,
|
|
14
|
+
CatalogContentsResultSchema,
|
|
15
|
+
SchemaContentsSchema,
|
|
16
|
+
} from "./vgi-protocol-schemas.js";
|
|
17
|
+
import type { CatalogAttachResult, CatalogContentsResponse, SchemaContents } from "./vgi-client.js";
|
|
18
|
+
export type { CatalogAttachResult, CatalogContentsResponse, SchemaContents };
|
|
19
|
+
|
|
20
|
+
// ----------------------------------------------------------------------------
|
|
21
|
+
// CatalogAttachResult (schema: CatalogAttachResultSchema)
|
|
22
|
+
// ----------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/** Fields of `CatalogAttachResult` that have a protocol default. */
|
|
25
|
+
export type CatalogAttachResultDefaultedField =
|
|
26
|
+
| "attach_opaque_data_required"
|
|
27
|
+
| "default_schema"
|
|
28
|
+
| "settings"
|
|
29
|
+
| "secret_types"
|
|
30
|
+
| "attach_catalogs"
|
|
31
|
+
| "comment"
|
|
32
|
+
| "tags"
|
|
33
|
+
| "supports_column_statistics"
|
|
34
|
+
| "global_functions"
|
|
35
|
+
| "global_function_prefix"
|
|
36
|
+
| "supports_catalog_contents";
|
|
37
|
+
/** `CatalogAttachResult` with every field that has a protocol default optional. */
|
|
38
|
+
export type CatalogAttachResultInit = Omit<CatalogAttachResult, CatalogAttachResultDefaultedField> & Partial<Pick<CatalogAttachResult, CatalogAttachResultDefaultedField>>;
|
|
39
|
+
|
|
40
|
+
/** Build a complete `CatalogAttachResult`, filling the protocol defaults, in wire order. */
|
|
41
|
+
export function buildCatalogAttachResult(init: CatalogAttachResultInit): CatalogAttachResult {
|
|
42
|
+
return {
|
|
43
|
+
attach_opaque_data: init.attach_opaque_data,
|
|
44
|
+
supports_transactions: init.supports_transactions,
|
|
45
|
+
supports_time_travel: init.supports_time_travel,
|
|
46
|
+
catalog_version_frozen: init.catalog_version_frozen,
|
|
47
|
+
catalog_version: init.catalog_version,
|
|
48
|
+
attach_opaque_data_required: init.attach_opaque_data_required ?? true,
|
|
49
|
+
default_schema: init.default_schema ?? "main",
|
|
50
|
+
settings: init.settings ?? [],
|
|
51
|
+
secret_types: init.secret_types ?? [],
|
|
52
|
+
attach_catalogs: init.attach_catalogs ?? [],
|
|
53
|
+
comment: init.comment ?? null,
|
|
54
|
+
tags: init.tags ?? {},
|
|
55
|
+
supports_column_statistics: init.supports_column_statistics ?? false,
|
|
56
|
+
global_functions: init.global_functions ?? [],
|
|
57
|
+
global_function_prefix: init.global_function_prefix ?? "",
|
|
58
|
+
resolved_data_version: init.resolved_data_version ?? null,
|
|
59
|
+
resolved_implementation_version: init.resolved_implementation_version ?? null,
|
|
60
|
+
supports_catalog_contents: init.supports_catalog_contents ?? false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const encodeCatalogAttachResult = (v: CatalogAttachResult): Uint8Array => encodeASD(CatalogAttachResultSchema, v);
|
|
65
|
+
export const decodeCatalogAttachResult = (b: Uint8Array): CatalogAttachResult => decodeASD<CatalogAttachResult>(CatalogAttachResultSchema, b);
|
|
66
|
+
|
|
67
|
+
// ----------------------------------------------------------------------------
|
|
68
|
+
// SchemaContents (schema: SchemaContentsSchema)
|
|
69
|
+
// ----------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/** Fields of `SchemaContents` that have a protocol default. */
|
|
72
|
+
export type SchemaContentsDefaultedField =
|
|
73
|
+
| "tables"
|
|
74
|
+
| "views"
|
|
75
|
+
| "scalar_functions"
|
|
76
|
+
| "aggregate_functions"
|
|
77
|
+
| "table_functions"
|
|
78
|
+
| "scalar_macros"
|
|
79
|
+
| "table_macros"
|
|
80
|
+
| "indexes";
|
|
81
|
+
/** `SchemaContents` with every field that has a protocol default optional. */
|
|
82
|
+
export type SchemaContentsInit = Omit<SchemaContents, SchemaContentsDefaultedField> & Partial<Pick<SchemaContents, SchemaContentsDefaultedField>>;
|
|
83
|
+
|
|
84
|
+
/** Build a complete `SchemaContents`, filling the protocol defaults, in wire order. */
|
|
85
|
+
export function buildSchemaContents(init: SchemaContentsInit): SchemaContents {
|
|
86
|
+
return {
|
|
87
|
+
path: init.path,
|
|
88
|
+
schema: init.schema,
|
|
89
|
+
tables: init.tables ?? [],
|
|
90
|
+
views: init.views ?? [],
|
|
91
|
+
scalar_functions: init.scalar_functions ?? [],
|
|
92
|
+
aggregate_functions: init.aggregate_functions ?? [],
|
|
93
|
+
table_functions: init.table_functions ?? [],
|
|
94
|
+
scalar_macros: init.scalar_macros ?? [],
|
|
95
|
+
table_macros: init.table_macros ?? [],
|
|
96
|
+
indexes: init.indexes ?? [],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const encodeSchemaContents = (v: SchemaContents): Uint8Array => encodeASD(SchemaContentsSchema, v);
|
|
101
|
+
export const decodeSchemaContents = (b: Uint8Array): SchemaContents => decodeASD<SchemaContents>(SchemaContentsSchema, b);
|
|
102
|
+
|
|
103
|
+
// ----------------------------------------------------------------------------
|
|
104
|
+
// CatalogContentsResponse (schema: CatalogContentsResultSchema)
|
|
105
|
+
// ----------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/** Fields of `CatalogContentsResponse` that have a protocol default. */
|
|
108
|
+
export type CatalogContentsResponseDefaultedField =
|
|
109
|
+
| "etag"
|
|
110
|
+
| "not_modified"
|
|
111
|
+
| "schemas";
|
|
112
|
+
/** `CatalogContentsResponse` with every field that has a protocol default optional. */
|
|
113
|
+
export type CatalogContentsResponseInit = Omit<CatalogContentsResponse, CatalogContentsResponseDefaultedField> & Partial<Pick<CatalogContentsResponse, CatalogContentsResponseDefaultedField>>;
|
|
114
|
+
|
|
115
|
+
/** Build a complete `CatalogContentsResponse`, filling the protocol defaults, in wire order. */
|
|
116
|
+
export function buildCatalogContentsResponse(init: CatalogContentsResponseInit): CatalogContentsResponse {
|
|
117
|
+
return {
|
|
118
|
+
catalog_version: init.catalog_version,
|
|
119
|
+
etag: init.etag ?? null,
|
|
120
|
+
not_modified: init.not_modified ?? false,
|
|
121
|
+
schemas: init.schemas ?? [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const encodeCatalogContentsResponse = (v: CatalogContentsResponse): Uint8Array => encodeASD(CatalogContentsResultSchema, v);
|
|
126
|
+
export const decodeCatalogContentsResponse = (b: Uint8Array): CatalogContentsResponse => decodeASD<CatalogContentsResponse>(CatalogContentsResultSchema, b);
|
package/src/http/fetch.ts
CHANGED
|
@@ -25,9 +25,10 @@ import {
|
|
|
25
25
|
import { arrowStateSerializer } from "../protocol/state-serializer.js";
|
|
26
26
|
import { buildVgiProtocol, type ProtocolConfig } from "../protocol/dispatch.js";
|
|
27
27
|
import { createLandingRoutes, type LandingInfo } from "./landing.js";
|
|
28
|
+
import { buildRpcServer, type HostingOptions } from "../rpc-server.js";
|
|
28
29
|
import { setRequestAuthScope, type RequestAuthHolder } from "../request-auth.js";
|
|
29
30
|
|
|
30
|
-
export interface VgiFetchOptions {
|
|
31
|
+
export interface VgiFetchOptions extends HostingOptions {
|
|
31
32
|
/** Wire-protocol config (registry + catalogInterface). */
|
|
32
33
|
protocol: ProtocolConfig;
|
|
33
34
|
/** HMAC key for state-token signing. Pass a stable, secret 32-byte key
|
|
@@ -197,7 +198,14 @@ export function createVgiFetch(opts: VgiFetchOptions): (req: Request) => Promise
|
|
|
197
198
|
...opts.protocol,
|
|
198
199
|
});
|
|
199
200
|
|
|
200
|
-
|
|
201
|
+
// The same builder every other transport uses: `vgi.v2`, the worker's
|
|
202
|
+
// hostedProtocols(), reflection, and -- HTTP being the transport that
|
|
203
|
+
// authenticates callers -- vgi_rpc.Identity.v1 when the worker opts in.
|
|
204
|
+
// Built at construction, so a worker that opts into introspection without an
|
|
205
|
+
// allowlist fails here rather than serving traffic.
|
|
206
|
+
const server = buildRpcServer(protocol, opts, { transport: "http", serverId });
|
|
207
|
+
|
|
208
|
+
const handler = createHttpHandler(server, {
|
|
201
209
|
prefix,
|
|
202
210
|
serverId,
|
|
203
211
|
tokenKey: opts.signingKey,
|
package/src/index.core.ts
CHANGED
|
@@ -77,9 +77,9 @@ export {
|
|
|
77
77
|
export type { VgiFunction, FunctionMeta, StreamHandlers, FunctionExample, HandlerState } from "./functions/types.js";
|
|
78
78
|
|
|
79
79
|
// Catalog
|
|
80
|
-
export { CatalogInterface, type CatalogAttachResult, type SchemaInfo, encodeSchemaInfo, decodeSchemaInfo, type TableInfo, encodeTableInfo, decodeTableInfo, type ViewInfo, encodeViewInfo, decodeViewInfo, type CatalogInfo, encodeCatalogInfo, decodeCatalogInfo, type FunctionInfo, encodeFunctionInfo, decodeFunctionInfo, type FunctionInfoOptions, type MacroInfo, encodeMacroInfo, decodeMacroInfo, type MacroType, type CopyFromFormatInfo, encodeCopyFromFormatInfo, type AttachOpaqueData, type TransactionOpaqueData, buildScanBranchesResult, encodeFormatOptions, type ScanBranchInput } from "./catalog/interface.js";
|
|
80
|
+
export { CatalogInterface, type CatalogAttachResult, type CatalogAttachResultInit, buildCatalogAttachResult, type SchemaContentsInfo, type CatalogContentsResult, type SchemaInfo, encodeSchemaInfo, decodeSchemaInfo, type TableInfo, encodeTableInfo, decodeTableInfo, type ViewInfo, encodeViewInfo, decodeViewInfo, type CatalogInfo, encodeCatalogInfo, decodeCatalogInfo, type FunctionInfo, encodeFunctionInfo, decodeFunctionInfo, type FunctionInfoOptions, type MacroInfo, encodeMacroInfo, decodeMacroInfo, type MacroType, type CopyFromFormatInfo, encodeCopyFromFormatInfo, type AttachOpaqueData, type TransactionOpaqueData, buildScanBranchesResult, encodeFormatOptions, type ScanBranchInput } from "./catalog/interface.js";
|
|
81
81
|
export type { CatalogDescriptor, SchemaDescriptor, TableDescriptor, ViewDescriptor, MacroDescriptor, SettingDescriptor, SecretTypeDescriptor, ForeignKeyDef, DefaultValue } from "./catalog/descriptors.js";
|
|
82
|
-
export { ReadOnlyCatalogInterface } from "./catalog/read-only.js";
|
|
82
|
+
export { ReadOnlyCatalogInterface, READ_ONLY_ITEM_ATTACH_ID } from "./catalog/read-only.js";
|
|
83
83
|
export { CompositeCatalogInterface } from "./catalog/composite.js";
|
|
84
84
|
export { FunctionRegistry } from "./functions/registry.js";
|
|
85
85
|
|
|
@@ -303,10 +303,31 @@ export type {
|
|
|
303
303
|
CatalogFunctionType as ClientCatalogFunctionType,
|
|
304
304
|
CatalogAttachOptions,
|
|
305
305
|
AttachOptionValue,
|
|
306
|
+
CatalogSnapshot,
|
|
307
|
+
LoadCatalogOptions,
|
|
306
308
|
} from "./client/types.js";
|
|
307
309
|
|
|
308
310
|
// Re-export from vgi-rpc for convenience
|
|
309
311
|
export { str, bytes, int, int32, float, float32, bool, toSchema, OutputCollector, AuthContext } from "@query-farm/vgi-rpc";
|
|
310
312
|
export { subprocessConnect, httpConnect, tcpConnect } from "@query-farm/vgi-rpc";
|
|
311
313
|
export type { RpcClient, StreamSession, LogMessage } from "@query-farm/vgi-rpc";
|
|
314
|
+
|
|
315
|
+
// Hosting additional protocols and opting into vgi_rpc.Identity.v1. The errors
|
|
316
|
+
// are re-exported so a worker's resolveToken/mintGrant can throw the right one:
|
|
317
|
+
// AuthUnavailableError (or IdentityUnavailableError) for "could not find out",
|
|
318
|
+
// GrantRefusedError to decline a grant.
|
|
319
|
+
export {
|
|
320
|
+
buildRpcServer,
|
|
321
|
+
INTROSPECT_PRINCIPALS_ENV,
|
|
322
|
+
type BuildRpcServerOptions,
|
|
323
|
+
type HostingOptions,
|
|
324
|
+
type ServerTransport,
|
|
325
|
+
} from "./rpc-server.js";
|
|
326
|
+
export {
|
|
327
|
+
AuthUnavailableError,
|
|
328
|
+
GrantRefusedError,
|
|
329
|
+
IdentityUnavailableError,
|
|
330
|
+
StatusError,
|
|
331
|
+
} from "@query-farm/vgi-rpc";
|
|
332
|
+
export type { GrantMinter, IssuedGrant, TokenIdentity, TokenResolver } from "@query-farm/vgi-rpc";
|
|
312
333
|
export { normalizeSchemaPath, schemaPathDisplay, schemaPathKey, schemaPathsEqual, type SchemaPath } from "./schema-path.js";
|
package/src/protocol/dispatch.ts
CHANGED
|
@@ -57,7 +57,7 @@ export interface ProtocolConfig {
|
|
|
57
57
|
export const VGI_PROTOCOL_NAME = "vgi.v2";
|
|
58
58
|
|
|
59
59
|
export function buildVgiProtocol(config: ProtocolConfig): Protocol {
|
|
60
|
-
const protocol = new Protocol(VGI_PROTOCOL_NAME, { protocolVersion: "2.
|
|
60
|
+
const protocol = new Protocol(VGI_PROTOCOL_NAME, { protocolVersion: "2.1.0" });
|
|
61
61
|
|
|
62
62
|
registerFunctionMethods(protocol, {
|
|
63
63
|
registry: config.registry,
|
|
@@ -4,12 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
import { type VgiSchema, schema, type VgiField, field, type VgiDataType, binary, utf8, bool } from "../../../arrow/index.js";
|
|
6
6
|
import { Protocol, type CallContext } from "@query-farm/vgi-rpc";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
encodeSchemaInfo,
|
|
9
|
+
encodeTableInfo,
|
|
10
|
+
encodeViewInfo,
|
|
11
|
+
encodeCatalogInfo,
|
|
12
|
+
} from "../../../generated/vgi-client.js";
|
|
13
|
+
import { buildCatalogAttachResult } from "../../../generated/vgi-protocol-types.js";
|
|
14
|
+
import { serveCatalogContents } from "./contents.js";
|
|
8
15
|
import { encodeFunctionInfoOnce } from "../../../catalog/item-encoding.js";
|
|
9
16
|
import { encodeCopyFromFormatInfo } from "../../../catalog/interface.js";
|
|
10
17
|
import {
|
|
11
18
|
CatalogAttachParamsSchema,
|
|
12
19
|
CatalogAttachResultSchema,
|
|
20
|
+
CatalogContentsParamsSchema,
|
|
13
21
|
CatalogCatalogsParamsSchema,
|
|
14
22
|
CatalogCatalogsResultSchema,
|
|
15
23
|
CatalogCopyFromFormatsParamsSchema,
|
|
@@ -105,33 +113,19 @@ export function registerCatalogAdminMethods(protocol: Protocol, getCatalog: GetC
|
|
|
105
113
|
(ctx as CallContext | undefined)?.auth,
|
|
106
114
|
signingKey,
|
|
107
115
|
);
|
|
108
|
-
|
|
116
|
+
// The generated builder emits every column of the pinned
|
|
117
|
+
// CatalogAttachResult schema, filling vgi-python's defaults for any a
|
|
118
|
+
// catalog left out (supports_catalog_contents=false: the client then
|
|
119
|
+
// keeps to the per-schema RPCs).
|
|
120
|
+
return wrapResult(buildCatalogAttachResult({
|
|
121
|
+
...result,
|
|
109
122
|
attach_opaque_data: sealedAttach,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
attach_opaque_data_required: result.attach_opaque_data_required ?? true,
|
|
115
|
-
default_schema: result.default_schema ?? "main",
|
|
116
|
-
settings: result.settings ?? [],
|
|
117
|
-
secret_types: result.secret_types ?? [],
|
|
118
|
-
attach_catalogs: result.attach_catalogs ?? [],
|
|
119
|
-
comment: result.comment ?? null,
|
|
120
|
-
tags: result.tags ?? {},
|
|
121
|
-
// True so DuckDB will route catalog_table_column_statistics_get RPCs
|
|
122
|
-
// to our handler for tables whose TableInfo.supports_column_statistics
|
|
123
|
-
// is also true. Catalogs that never serve column stats can override
|
|
124
|
-
// this in attach() via CatalogAttachResult.supports_column_statistics.
|
|
123
|
+
// SDK default, unlike vgi-python's false: true so DuckDB will route
|
|
124
|
+
// catalog_table_column_statistics_get RPCs to our handler for tables
|
|
125
|
+
// whose TableInfo.supports_column_statistics is also true. Catalogs
|
|
126
|
+
// that never serve column stats set it false in attach().
|
|
125
127
|
supports_column_statistics: result.supports_column_statistics ?? true,
|
|
126
|
-
|
|
127
|
-
// its global namespace. Empty unless the catalog advertises them —
|
|
128
|
-
// but the columns must still be present, since the extension pins the
|
|
129
|
-
// full CatalogAttachResult field list on the response schema.
|
|
130
|
-
global_functions: result.global_functions ?? [],
|
|
131
|
-
global_function_prefix: result.global_function_prefix ?? "",
|
|
132
|
-
resolved_data_version: result.resolved_data_version ?? null,
|
|
133
|
-
resolved_implementation_version: result.resolved_implementation_version ?? null,
|
|
134
|
-
}, CatalogAttachResultSchema);
|
|
128
|
+
}), CatalogAttachResultSchema);
|
|
135
129
|
},
|
|
136
130
|
});
|
|
137
131
|
|
|
@@ -247,6 +241,21 @@ export function registerCatalogAdminMethods(protocol: Protocol, getCatalog: GetC
|
|
|
247
241
|
},
|
|
248
242
|
});
|
|
249
243
|
|
|
244
|
+
// catalog_contents — every schema and all of its contents in one result,
|
|
245
|
+
// for a client whose attach result set supports_catalog_contents. Takes no
|
|
246
|
+
// transaction: the client caches the answer for the whole attach, so it is
|
|
247
|
+
// the committed catalog at catalog_version. `if_none_match` revalidates a
|
|
248
|
+
// snapshot the client holds (etag / not_modified); see ./contents.ts for the
|
|
249
|
+
// rules, the content-hash etag and the frozen-catalog cache.
|
|
250
|
+
catalogUnary(protocol, signingKey, "catalog_contents", {
|
|
251
|
+
params: CatalogContentsParamsSchema,
|
|
252
|
+
result: RESULT_BINARY_SCHEMA,
|
|
253
|
+
handler: async (params) => {
|
|
254
|
+
const ifNoneMatch = params.if_none_match == null ? null : String(params.if_none_match);
|
|
255
|
+
return serveCatalogContents(getCatalog(), toUint8Array(params.attach_opaque_data), ifNoneMatch);
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
250
259
|
// catalog_copy_from_formats — catalog-level (not schema-scoped). Lists the
|
|
251
260
|
// custom COPY ... FROM formats this catalog advertises; empty list when none.
|
|
252
261
|
catalogUnary(protocol, signingKey, "catalog_copy_from_formats", {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
// SPDX-License-Identifier: LicenseRef-QueryFarm-Source-Available-1.0
|
|
3
|
+
|
|
4
|
+
// catalog_contents: the worker side of the protocol 2.1.0 bulk enumeration,
|
|
5
|
+
// with etag revalidation and the frozen-catalog response cache. Mirrors
|
|
6
|
+
// vgi-python's Worker.catalog_contents / _catalog_contents_response.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
encodeSchemaInfo,
|
|
10
|
+
encodeTableInfo,
|
|
11
|
+
encodeViewInfo,
|
|
12
|
+
encodeMacroInfo,
|
|
13
|
+
encodeIndexInfo,
|
|
14
|
+
} from "../../../generated/vgi-client.js";
|
|
15
|
+
import {
|
|
16
|
+
buildCatalogContentsResponse,
|
|
17
|
+
buildSchemaContents,
|
|
18
|
+
type CatalogContentsResponse,
|
|
19
|
+
type SchemaContents,
|
|
20
|
+
} from "../../../generated/vgi-protocol-types.js";
|
|
21
|
+
import { CatalogContentsResultSchema } from "../../../generated/vgi-protocol-schemas.js";
|
|
22
|
+
import { encodeFunctionInfoOnce } from "../../../catalog/item-encoding.js";
|
|
23
|
+
import type { AttachOpaqueData, CatalogInterface } from "../../../catalog/interface.js";
|
|
24
|
+
import { catalogContentsDigest } from "../../../catalog/contents-digest.js";
|
|
25
|
+
import { schemaPathKey } from "../../../schema-path.js";
|
|
26
|
+
import { wrapResult } from "../shared.js";
|
|
27
|
+
|
|
28
|
+
type Wire = { result: Uint8Array };
|
|
29
|
+
|
|
30
|
+
/** One cached answer per catalog instance, valid for one catalog version. */
|
|
31
|
+
interface ContentsSnapshot {
|
|
32
|
+
version: number;
|
|
33
|
+
etag: string | null;
|
|
34
|
+
wire: Wire;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Cached catalog_contents responses, keyed by the owning catalog instance
|
|
39
|
+
* (weak, so a dropped catalog takes its snapshot with it). Holds the build
|
|
40
|
+
* promise, so concurrent first calls build once.
|
|
41
|
+
*/
|
|
42
|
+
const CACHE = new WeakMap<CatalogInterface, Promise<ContentsSnapshot>>();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Ask the catalog for its contents and shape the typed wire response.
|
|
46
|
+
*
|
|
47
|
+
* Enforces the revalidation rules: `not_modified` needs an etag equal to
|
|
48
|
+
* `ifNoneMatch` and no schemas; a catalog with no etag never yields
|
|
49
|
+
* `not_modified` (and so ignores `ifNoneMatch`). Validates the schema paths
|
|
50
|
+
* (unique, parent present) and orders them parent-first. Every item is
|
|
51
|
+
* encoded with the SAME encoder its per-schema RPC uses, so items are
|
|
52
|
+
* byte-identical to catalog_schemas / catalog_schema_contents_* items.
|
|
53
|
+
*/
|
|
54
|
+
export async function buildCatalogContents(
|
|
55
|
+
cat: CatalogInterface,
|
|
56
|
+
owner: CatalogInterface,
|
|
57
|
+
attach: AttachOpaqueData,
|
|
58
|
+
version: number,
|
|
59
|
+
ifNoneMatch: string | null,
|
|
60
|
+
): Promise<CatalogContentsResponse> {
|
|
61
|
+
const result = await cat.catalogContents(attach, ifNoneMatch);
|
|
62
|
+
const etag0 = result.etag ?? null;
|
|
63
|
+
if (result.not_modified) {
|
|
64
|
+
if (etag0 === null || ifNoneMatch === null || etag0 !== ifNoneMatch) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"catalog_contents returned not_modified, but only a catalog whose etag equals " +
|
|
67
|
+
"if_none_match may (and it must return that etag)",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (result.schemas && result.schemas.length > 0) {
|
|
71
|
+
throw new Error("catalog_contents returned not_modified with schemas; it must return none");
|
|
72
|
+
}
|
|
73
|
+
return buildCatalogContentsResponse({ catalog_version: version, etag: etag0, not_modified: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const infos = [...(result.schemas ?? [])];
|
|
77
|
+
const keys = infos.map((c) => schemaPathKey(c.schema.path));
|
|
78
|
+
const keySet = new Set(keys);
|
|
79
|
+
if (keySet.size !== keys.length) throw new Error("catalog_contents returned duplicate schema paths");
|
|
80
|
+
for (const c of infos) {
|
|
81
|
+
if (c.schema.path.length > 1 && !keySet.has(schemaPathKey(c.schema.path.slice(0, -1)))) {
|
|
82
|
+
throw new Error(`catalog_contents returned schema path ${JSON.stringify(c.schema.path)} without its parent`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Same parent-before-child order catalog_schemas guarantees (stable sort).
|
|
87
|
+
const schemas: SchemaContents[] = infos
|
|
88
|
+
.sort((a, b) => a.schema.path.length - b.schema.path.length)
|
|
89
|
+
.map((c) =>
|
|
90
|
+
buildSchemaContents({
|
|
91
|
+
path: [...c.schema.path],
|
|
92
|
+
schema: encodeSchemaInfo(c.schema),
|
|
93
|
+
tables: c.tables.map((t) => encodeTableInfo(t)),
|
|
94
|
+
views: c.views.map((v) => encodeViewInfo(v)),
|
|
95
|
+
scalar_functions: c.scalar_functions.map((f) => encodeFunctionInfoOnce(f)),
|
|
96
|
+
aggregate_functions: c.aggregate_functions.map((f) => encodeFunctionInfoOnce(f)),
|
|
97
|
+
table_functions: c.table_functions.map((f) => encodeFunctionInfoOnce(f)),
|
|
98
|
+
scalar_macros: c.scalar_macros.map((m) => encodeMacroInfo(m)),
|
|
99
|
+
table_macros: c.table_macros.map((m) => encodeMacroInfo(m)),
|
|
100
|
+
indexes: c.indexes.map((i) => encodeIndexInfo(i)),
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
let etag = etag0;
|
|
104
|
+
if (etag === null && owner.catalogContentsEtag === "content-hash") {
|
|
105
|
+
etag = await catalogContentsDigest(schemas);
|
|
106
|
+
}
|
|
107
|
+
if (etag !== null && ifNoneMatch !== null && etag === ifNoneMatch) {
|
|
108
|
+
return buildCatalogContentsResponse({ catalog_version: version, etag, not_modified: true });
|
|
109
|
+
}
|
|
110
|
+
return buildCatalogContentsResponse({ catalog_version: version, etag, schemas });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Serve one catalog_contents call (the attach value is already opened).
|
|
115
|
+
*
|
|
116
|
+
* Caching: a catalog whose `catalogContentsCacheable()` is true (by default:
|
|
117
|
+
* version frozen and contents attach-independent; ReadOnlyCatalogInterface) is
|
|
118
|
+
* built once per (catalog instance, version); every later call reuses the same
|
|
119
|
+
* serialized response bytes. A conditional call matching the cached etag is
|
|
120
|
+
* answered `not_modified` without touching the snapshot.
|
|
121
|
+
*/
|
|
122
|
+
export async function serveCatalogContents(
|
|
123
|
+
cat: CatalogInterface,
|
|
124
|
+
attach: AttachOpaqueData,
|
|
125
|
+
ifNoneMatch: string | null,
|
|
126
|
+
): Promise<Wire> {
|
|
127
|
+
const owner = cat.catalogContentsOwner(attach);
|
|
128
|
+
const version = Number(await cat.version(attach));
|
|
129
|
+
if (owner.catalogContentsCacheable()) {
|
|
130
|
+
let pending = CACHE.get(owner);
|
|
131
|
+
let snapshot = pending ? await pending.catch(() => null) : null;
|
|
132
|
+
if (!snapshot || snapshot.version !== version) {
|
|
133
|
+
pending = (async () => {
|
|
134
|
+
const built = await buildCatalogContents(cat, owner, attach, version, null);
|
|
135
|
+
return {
|
|
136
|
+
version,
|
|
137
|
+
etag: built.etag ?? null,
|
|
138
|
+
wire: wrapResult(built, CatalogContentsResultSchema),
|
|
139
|
+
};
|
|
140
|
+
})();
|
|
141
|
+
CACHE.set(owner, pending);
|
|
142
|
+
try {
|
|
143
|
+
snapshot = await pending;
|
|
144
|
+
} catch (err) {
|
|
145
|
+
// A failed build is not cached: the next call tries again.
|
|
146
|
+
if (CACHE.get(owner) === pending) CACHE.delete(owner);
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (ifNoneMatch !== null && snapshot.etag !== null && ifNoneMatch === snapshot.etag) {
|
|
151
|
+
return wrapResult(
|
|
152
|
+
buildCatalogContentsResponse({ catalog_version: version, etag: snapshot.etag, not_modified: true }),
|
|
153
|
+
CatalogContentsResultSchema,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return snapshot.wire;
|
|
157
|
+
}
|
|
158
|
+
return wrapResult(
|
|
159
|
+
await buildCatalogContents(cat, owner, attach, version, ifNoneMatch),
|
|
160
|
+
CatalogContentsResultSchema,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -52,7 +52,8 @@ export function registerCatalogMacroMethods(protocol: Protocol, getCatalog: GetC
|
|
|
52
52
|
result: emptyResultSchema,
|
|
53
53
|
handler: async (params) => {
|
|
54
54
|
const cat = getCatalog();
|
|
55
|
-
|
|
55
|
+
// catalogUnary already flattened the wrapped request.
|
|
56
|
+
const innerParams = params;
|
|
56
57
|
await cat.macroCreate(
|
|
57
58
|
toUint8Array(innerParams.attach_opaque_data),
|
|
58
59
|
innerParams.schema_path,
|
|
@@ -7,7 +7,7 @@ import type { CatalogInterface } from "../../../catalog/interface.js";
|
|
|
7
7
|
import { NoCatalogError } from "../../../errors.js";
|
|
8
8
|
import { deserializeBatch } from "../../../util/arrow/index.js";
|
|
9
9
|
import { toUint8Array } from "../../../util/bytes.js";
|
|
10
|
-
import { normalizeRequestSchemaPaths } from "../shared.js";
|
|
10
|
+
import { normalizeRequestSchemaPaths, unwrapRequest } from "../shared.js";
|
|
11
11
|
import {
|
|
12
12
|
sealBytes,
|
|
13
13
|
openBytes,
|
|
@@ -195,6 +195,17 @@ export function catalogUnary(
|
|
|
195
195
|
result: config.result as any,
|
|
196
196
|
doc: config.doc,
|
|
197
197
|
handler: async (params: Record<string, any>, ctx: any) => {
|
|
198
|
+
// catalog_create / catalog_table_create / catalog_macro_create (and
|
|
199
|
+
// catalog_index_create) send one `request: binary` column holding the
|
|
200
|
+
// IPC-serialized request record, so their attach/transaction values ride
|
|
201
|
+
// INSIDE it. Flatten it first, so handlers read the request's fields
|
|
202
|
+
// directly and those values are opened (and the framework UUID stripped)
|
|
203
|
+
// like every other catalog call's. Before this, table_create read every
|
|
204
|
+
// field off the envelope (all undefined) and macro_create handed the
|
|
205
|
+
// catalog an unopened attach value.
|
|
206
|
+
if (params.request != null && params.attach_opaque_data === undefined && params.name === undefined) {
|
|
207
|
+
params = unwrapRequest(params.request);
|
|
208
|
+
}
|
|
198
209
|
normalizeRequestSchemaPaths(params);
|
|
199
210
|
await unwrapParamsOpaque(params, ctx, signingKey);
|
|
200
211
|
return config.handler(params, ctx);
|