@query-farm/vgi 0.37.1 → 0.38.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 (46) hide show
  1. package/README.md +74 -0
  2. package/dist/catalog/composite.d.ts +23 -1
  3. package/dist/catalog/contents-digest.d.ts +25 -0
  4. package/dist/catalog/interface.d.ts +107 -2
  5. package/dist/catalog/read-only.d.ts +45 -1
  6. package/dist/client/client.d.ts +19 -1
  7. package/dist/client-entry.js +141 -4
  8. package/dist/client-entry.js.map +9 -8
  9. package/dist/codec/asd.d.ts +8 -0
  10. package/dist/generated/vgi-client.d.ts +19 -0
  11. package/dist/generated/vgi-protocol-schemas.d.ts +3 -0
  12. package/dist/generated/vgi-protocol-types.d.ts +26 -0
  13. package/dist/http/fetch.d.ts +2 -1
  14. package/dist/index.core.d.ts +5 -2
  15. package/dist/index.js +574 -36
  16. package/dist/index.js.map +20 -16
  17. package/dist/protocol/handlers/catalog/contents.d.ts +27 -0
  18. package/dist/rpc-server.d.ts +97 -0
  19. package/dist/serve-entry.d.ts +2 -1
  20. package/dist/serve-entry.js +405 -25
  21. package/dist/serve-entry.js.map +18 -14
  22. package/dist/worker-cf-entry.d.ts +3 -0
  23. package/dist/worker-cf-entry.js +528 -31
  24. package/dist/worker-cf-entry.js.map +20 -16
  25. package/dist/worker.d.ts +9 -1
  26. package/package.json +3 -3
  27. package/src/catalog/composite.ts +81 -0
  28. package/src/catalog/contents-digest.ts +78 -0
  29. package/src/catalog/interface.ts +167 -5
  30. package/src/catalog/read-only.ts +68 -6
  31. package/src/client/client.ts +53 -0
  32. package/src/codec/asd.ts +96 -1
  33. package/src/generated/vgi-client.ts +22 -1
  34. package/src/generated/vgi-protocol-schemas.ts +30 -1
  35. package/src/generated/vgi-protocol-types.ts +126 -0
  36. package/src/http/fetch.ts +10 -2
  37. package/src/index.core.ts +21 -2
  38. package/src/protocol/dispatch.ts +1 -1
  39. package/src/protocol/handlers/catalog/admin.ts +35 -26
  40. package/src/protocol/handlers/catalog/contents.ts +162 -0
  41. package/src/protocol/handlers/catalog/macro.ts +2 -1
  42. package/src/protocol/handlers/catalog/shared.ts +12 -1
  43. package/src/rpc-server.ts +273 -0
  44. package/src/serve-entry.ts +7 -1
  45. package/src/worker-cf-entry.ts +8 -0
  46. package/src/worker.ts +24 -6
package/README.md CHANGED
@@ -479,6 +479,7 @@ A worker serves the same functions over any of:
479
479
  - **AF_UNIX** — a long-lived warm worker (`--unix <path>`), reused across calls.
480
480
  - **HTTP** — stateless; all state round-trips in a self-contained token, so requests
481
481
  can be load-balanced across hosts.
482
+ - **TCP** (`--tcp [HOST:]PORT`) and caller-provided byte streams (`worker.serveStream`).
482
483
 
483
484
  Transport handling lives in [`@query-farm/vgi-rpc`](https://www.npmjs.com/package/@query-farm/vgi-rpc).
484
485
 
@@ -541,6 +542,79 @@ its own required `name`/`doc`/`version`, so only the Cloudflare entry passes it
541
542
  explicitly. Without it there is no landing page and no `/vgi-client.js`; see
542
543
  [MIGRATION.md](MIGRATION.md).
543
544
 
545
+ ### Hosting additional protocols
546
+
547
+ A worker can serve other [vgi-rpc](https://www.npmjs.com/package/@query-farm/vgi-rpc)
548
+ protocols beside `vgi.v2`, on the same listener, through the `hostedProtocols` hook:
549
+
550
+ ```ts
551
+ import { Protocol, str } from "@query-farm/vgi-rpc";
552
+
553
+ const reports = new Protocol("acme.Reports.v1").unary("render", {
554
+ params: { id: str },
555
+ result: { result: str },
556
+ handler: ({ id }) => ({ result: `report ${id}` }),
557
+ });
558
+
559
+ new Worker({ functions, hostedProtocols: () => [reports] }).run();
560
+ // …or serveVgiWorker({ …, hostedProtocols: () => [reports] }),
561
+ // …or createVgiFetch({ …, hostedProtocols: () => [reports] }) on Cloudflare Workers.
562
+ ```
563
+
564
+ - The hook is called **once**, when the server is built; it may read configuration,
565
+ but its answer is fixed for the life of the process.
566
+ - Its protocols are hosted on **every** transport (stdio, AF_UNIX, TCP, the Iroh raw
567
+ upstream, `serveStream`, HTTP), after `vgi.v2`, in the order returned. Every
568
+ transport builds its server through one function, `buildRpcServer`.
569
+ - Each needs a distinct name, which may not be `vgi.v2` or start with the reserved
570
+ `vgi_rpc.`. Violations are a startup error naming `hostedProtocols()`.
571
+ - Requests route on their `vgi_rpc.protocol` key, so hosting more protocols never
572
+ changes how DuckDB's `vgi.v2` calls are dispatched.
573
+ - The protocol is the unit of optionality: an optional capability is its own protocol.
574
+
575
+ ### Token introspection and grants (`vgi_rpc.Identity.v1`)
576
+
577
+ A worker opts into `vgi_rpc.Identity.v1` by supplying `resolveToken` and/or
578
+ `mintGrant` (accepted by `Worker`, `serveVgiWorker`, `createVgiWorkerFetch` and
579
+ `createVgiFetch`). It is hosted on **HTTP only** — the transport that authenticates
580
+ callers — and only the methods whose hooks exist are hosted; with neither, the
581
+ protocol is absent.
582
+
583
+ ```ts
584
+ import { AuthUnavailableError } from "@query-farm/vgi";
585
+
586
+ serveVgiWorker({
587
+ // …
588
+ resolveToken: async (token) => {
589
+ let row;
590
+ try {
591
+ row = await apiKeys.lookup(token);
592
+ } catch (err) {
593
+ // "I could not find out" -- an outage, not a refusal.
594
+ throw new AuthUnavailableError("key store unreachable", 5);
595
+ }
596
+ return row ? { principal: row.principal, tokenName: row.label } : null; // null = unknown
597
+ },
598
+ introspectPrincipals: ["edge-proxy"], // or VGI_INTROSPECT_PRINCIPALS=edge-proxy
599
+ });
600
+ ```
601
+
602
+ **Enabling introspection without an allowlist refuses to start.** Authenticating
603
+ and introspecting are different capabilities; "any authenticated caller" would let
604
+ any user resolve any other user's credential to its owner. Set
605
+ `introspectPrincipals` or `VGI_INTROSPECT_PRINCIPALS` (comma-separated). `mintGrant`
606
+ alone needs no allowlist.
607
+
608
+ **Transient failures:** throw `AuthUnavailableError` (re-exported here from
609
+ `@query-farm/vgi-rpc`) from `resolveToken` or `mintGrant` when the answer is not
610
+ knowable — a store or sidecar outage, a timeout, a 5xx. The framework translates it
611
+ to `identity_unavailable` (code `UNAVAILABLE`) carrying that error's retry hint as
612
+ `vgi_rpc.RetryInfo`, so a caller retries instead of caching a refusal.
613
+ `IdentityUnavailableError` works too. Never throw a plain `Error` for an outage, and
614
+ never return `null` for one: both read as a definitive answer. Return `null` only
615
+ when the store answered and the credential is unknown; throw `GrantRefusedError` to
616
+ decline a grant.
617
+
544
618
  ## Runtimes & entry points
545
619
 
546
620
  The package ships a backend-agnostic Arrow facade and selects an implementation at
@@ -1,4 +1,4 @@
1
- import type { AttachOpaqueData, CatalogAttachResult, CatalogInfo, CopyFromFormatInfo, FunctionInfo, IndexInfo, MacroInfo, SchemaInfo, TableInfo, TransactionOpaqueData, ViewInfo } from "./interface.js";
1
+ import type { AttachOpaqueData, CatalogAttachResult, CatalogInfo, CopyFromFormatInfo, FunctionInfo, IndexInfo, MacroInfo, CatalogContentsResult, SchemaInfo, TableInfo, TransactionOpaqueData, ViewInfo } from "./interface.js";
2
2
  import { CatalogInterface } from "./interface.js";
3
3
  export declare class CompositeCatalogInterface extends CatalogInterface {
4
4
  private readonly _backends;
@@ -28,6 +28,28 @@ export declare class CompositeCatalogInterface extends CatalogInterface {
28
28
  } | null>;
29
29
  viewGet(attachOpaqueData: AttachOpaqueData, schemaPath: string[], name: string, transactionOpaqueData?: TransactionOpaqueData): Promise<ViewInfo | null>;
30
30
  macroGet(attachOpaqueData: AttachOpaqueData, schemaPath: string[], name: string, transactionOpaqueData?: TransactionOpaqueData): Promise<MacroInfo | null>;
31
+ schemaCreate(...args: Parameters<CatalogInterface["schemaCreate"]>): Promise<void>;
32
+ schemaDrop(...args: Parameters<CatalogInterface["schemaDrop"]>): Promise<void>;
33
+ tableCreate(...args: Parameters<CatalogInterface["tableCreate"]>): Promise<void>;
34
+ tableDrop(...args: Parameters<CatalogInterface["tableDrop"]>): Promise<void>;
35
+ tableCommentSet(...args: Parameters<CatalogInterface["tableCommentSet"]>): Promise<void>;
36
+ tableRename(...args: Parameters<CatalogInterface["tableRename"]>): Promise<void>;
37
+ tableColumnAdd(...args: Parameters<CatalogInterface["tableColumnAdd"]>): Promise<void>;
38
+ tableColumnDrop(...args: Parameters<CatalogInterface["tableColumnDrop"]>): Promise<void>;
39
+ tableColumnRename(...args: Parameters<CatalogInterface["tableColumnRename"]>): Promise<void>;
40
+ tableColumnDefaultSet(...args: Parameters<CatalogInterface["tableColumnDefaultSet"]>): Promise<void>;
41
+ tableColumnDefaultDrop(...args: Parameters<CatalogInterface["tableColumnDefaultDrop"]>): Promise<void>;
42
+ tableColumnTypeChange(...args: Parameters<CatalogInterface["tableColumnTypeChange"]>): Promise<void>;
43
+ tableNotNullSet(...args: Parameters<CatalogInterface["tableNotNullSet"]>): Promise<void>;
44
+ tableNotNullDrop(...args: Parameters<CatalogInterface["tableNotNullDrop"]>): Promise<void>;
45
+ viewCreate(...args: Parameters<CatalogInterface["viewCreate"]>): Promise<void>;
46
+ viewDrop(...args: Parameters<CatalogInterface["viewDrop"]>): Promise<void>;
47
+ viewRename(...args: Parameters<CatalogInterface["viewRename"]>): Promise<void>;
48
+ viewCommentSet(...args: Parameters<CatalogInterface["viewCommentSet"]>): Promise<void>;
49
+ macroCreate(...args: Parameters<CatalogInterface["macroCreate"]>): Promise<void>;
50
+ macroDrop(...args: Parameters<CatalogInterface["macroDrop"]>): Promise<void>;
51
+ catalogContents(attachOpaqueData: AttachOpaqueData, ifNoneMatch?: string | null): Promise<CatalogContentsResult>;
52
+ catalogContentsOwner(attachOpaqueData: AttachOpaqueData): CatalogInterface;
31
53
  copyFromFormats(attachOpaqueData: AttachOpaqueData, transactionOpaqueData?: TransactionOpaqueData): Promise<CopyFromFormatInfo[]>;
32
54
  transactionBegin(attachOpaqueData: AttachOpaqueData): Promise<Uint8Array | null>;
33
55
  transactionCommit(attachOpaqueData: AttachOpaqueData, transactionOpaqueData: TransactionOpaqueData): Promise<void>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The `content-hash` etag for `catalog_contents`.
3
+ *
4
+ * Mirrors vgi-python's `catalog_contents_digest()` byte for byte, so the same
5
+ * snapshot (the same item bytes) gives the same etag in every SDK.
6
+ */
7
+ import type { SchemaContents } from "../generated/vgi-protocol-types.js";
8
+ /**
9
+ * Hex SHA-256 over a `catalog_contents` snapshot: the `content-hash` etag.
10
+ *
11
+ * Covers every schema's path and the exact item bytes of every kind, in
12
+ * order, each length-prefixed (8-byte little-endian) so no two different
13
+ * snapshots share an input:
14
+ *
15
+ * u64(#schemas) { u64(#path) {u64(len) utf8(part)}* u64(len) schema
16
+ * ( u64(#items) {u64(len) item}* ) x 8 kinds }*
17
+ *
18
+ * Deterministic because the items' encoding is (map columns are written in
19
+ * sorted key order, see `encodeASD`), so two builds of the same catalog hash
20
+ * alike.
21
+ *
22
+ * @param schemas The snapshot, in wire order.
23
+ * @returns The lowercase hex digest.
24
+ */
25
+ export declare function catalogContentsDigest(schemas: readonly SchemaContents[]): Promise<string>;
@@ -2,8 +2,9 @@ export type AttachOpaqueData = Uint8Array;
2
2
  export type TransactionOpaqueData = Uint8Array;
3
3
  import type { MacroType } from "../generated/vgi-client.js";
4
4
  export type { MacroType };
5
- import type { CatalogAttachResult } from "../generated/vgi-client.js";
6
- export type { CatalogAttachResult };
5
+ import type { CatalogAttachResult, CatalogAttachResultInit } from "../generated/vgi-protocol-types.js";
6
+ export type { CatalogAttachResult, CatalogAttachResultInit };
7
+ export { buildCatalogAttachResult } from "../generated/vgi-protocol-types.js";
7
8
  import type { SchemaInfo } from "../generated/vgi-client.js";
8
9
  export type { SchemaInfo };
9
10
  export { encodeSchemaInfo, decodeSchemaInfo } from "../generated/vgi-client.js";
@@ -243,6 +244,45 @@ export declare function buildScanBranchesResult(branches: ScanBranchInput[], req
243
244
  branches: Uint8Array[];
244
245
  required_extensions: string[];
245
246
  };
247
+ /**
248
+ * One schema and everything in it, as returned by
249
+ * `CatalogInterface.catalogContents` (vgi-python's `SchemaContentsInfo`).
250
+ *
251
+ * Each kind is complete: an empty list means the schema has none of that kind.
252
+ * The `catalog_contents` handler encodes every item with the same encoder the
253
+ * matching per-schema RPC uses, so the items are byte-for-byte what
254
+ * `catalog_schemas` / `catalog_schema_contents_*` would return.
255
+ */
256
+ export interface SchemaContentsInfo {
257
+ schema: SchemaInfo;
258
+ tables: TableInfo[];
259
+ views: ViewInfo[];
260
+ scalar_functions: FunctionInfo[];
261
+ aggregate_functions: FunctionInfo[];
262
+ table_functions: FunctionInfo[];
263
+ scalar_macros: MacroInfo[];
264
+ table_macros: MacroInfo[];
265
+ indexes: IndexInfo[];
266
+ }
267
+ /**
268
+ * What `CatalogInterface.catalogContents` returns: a snapshot, or "not
269
+ * modified" (vgi-python's `CatalogContentsResult`).
270
+ *
271
+ * - `schemas`: one `SchemaContentsInfo` per schema. Must be empty (or absent)
272
+ * when `not_modified` is set.
273
+ * - `etag`: opaque validator for this snapshot (a generation counter, schema
274
+ * version, git sha, ...). The client sends it back as `if_none_match`.
275
+ * Null/absent means the catalog does not revalidate -- unless it opts in to
276
+ * the framework's content hash with `catalogContentsEtag = "content-hash"`.
277
+ * - `not_modified`: true when the request's `if_none_match` equals the
278
+ * current etag, so the catalog skipped building the snapshot. Requires
279
+ * `etag` (the matching validator).
280
+ */
281
+ export interface CatalogContentsResult {
282
+ schemas?: SchemaContentsInfo[];
283
+ etag?: string | null;
284
+ not_modified?: boolean;
285
+ }
246
286
  /**
247
287
  * Catalog method return type — sync `T` or `Promise<T>`. The framework's
248
288
  * dispatch handlers `await` every catalog call, so async overrides work
@@ -250,6 +290,36 @@ export declare function buildScanBranchesResult(branches: ScanBranchInput[], req
250
290
  */
251
291
  type Awaitable<T> = T | Promise<T>;
252
292
  export declare abstract class CatalogInterface {
293
+ /**
294
+ * Opt-in framework etag for `catalog_contents`. `"content-hash"`: when
295
+ * `catalogContents` returns no etag of its own, the worker uses the hex
296
+ * SHA-256 of the serialized snapshot (`catalogContentsDigest`) as the etag
297
+ * and turns a matching `if_none_match` into `not_modified` (it still builds
298
+ * the snapshot, but saves the transfer and the client's decode). `null`
299
+ * (default): no etag unless the catalog returns one. Off by default because
300
+ * for a catalog whose version is not frozen the client would revalidate with
301
+ * a full build on every transaction instead of a cheap `catalog_version`
302
+ * poll. Mirrors vgi-python's `catalog_contents_etag`.
303
+ */
304
+ catalogContentsEtag: "content-hash" | null;
305
+ /**
306
+ * Whether `catalogContents` depends only on `version()`: not on the attach
307
+ * (`attach_opaque_data`, attach options), the caller's identity, or anything
308
+ * else. Together with `catalogVersionFrozen`, it lets the worker build the
309
+ * `catalog_contents` response once per (catalog, version) and serve the same
310
+ * serialized bytes to every caller. Default false.
311
+ *
312
+ * `ReadOnlyCatalogInterface` sets it (its items carry a fixed attach id,
313
+ * like vgi-python's), unless its `perAttachItemIds` opt-in is on. A
314
+ * subclass whose contents vary per caller or attach option sets it false.
315
+ */
316
+ catalogContentsAttachIndependent: boolean;
317
+ /**
318
+ * Declares that `version()` never changes for this catalog instance (the
319
+ * attach result's `catalog_version_frozen`). Only the `catalog_contents`
320
+ * worker cache reads it. `ReadOnlyCatalogInterface` sets it.
321
+ */
322
+ catalogVersionFrozen: boolean;
253
323
  /**
254
324
  * Which catalog an `attach_opaque_data` belongs to, or null if unknown.
255
325
  *
@@ -348,6 +418,41 @@ export declare abstract class CatalogInterface {
348
418
  * vgi-python's `CatalogInterface.copy_from_formats`.
349
419
  */
350
420
  copyFromFormats(attachOpaqueData: AttachOpaqueData, transactionOpaqueData?: TransactionOpaqueData): Awaitable<CopyFromFormatInfo[]>;
421
+ /**
422
+ * Every schema and all of its contents, for the `catalog_contents` RPC.
423
+ *
424
+ * Served only to a client whose attach result set
425
+ * `supports_catalog_contents` (ReadOnlyCatalogInterface does). The default
426
+ * composes `schemas()` with the per-kind `schemaContents*()` calls, skipping
427
+ * a kind whose `estimated_object_count` is exactly 0 (a hard guarantee), and
428
+ * returns no etag -- so any catalog can serve it without new code. It runs
429
+ * with no transaction: the client caches the answer for the whole attach.
430
+ *
431
+ * Override it to build the snapshot more cheaply, or to revalidate: return
432
+ * an `etag` with the contents, and when `ifNoneMatch` equals the current
433
+ * etag return `{ etag, not_modified: true }` *before* building anything.
434
+ * Mirrors vgi-python's `CatalogInterface.catalog_contents`.
435
+ *
436
+ * @param ifNoneMatch The etag of the snapshot the client already holds, or
437
+ * null.
438
+ * @returns One `SchemaContentsInfo` per schema, in `schemas()` order (the
439
+ * handler sorts parents before children), plus its etag -- or
440
+ * `not_modified`.
441
+ */
442
+ catalogContents(attachOpaqueData: AttachOpaqueData, _ifNoneMatch?: string | null): Promise<CatalogContentsResult>;
443
+ /**
444
+ * Whether the worker may build `catalog_contents` once per (catalog
445
+ * instance, version) and reuse the serialized response for every caller:
446
+ * `catalogContentsAttachIndependent && catalogVersionFrozen` by default.
447
+ */
448
+ catalogContentsCacheable(): boolean;
449
+ /**
450
+ * The catalog that answers `catalog_contents` for this attach: whose
451
+ * `catalogContentsEtag` / `catalogContentsAttachIndependent` /
452
+ * `catalogVersionFrozen` the handler honours and whose instance keys the
453
+ * worker cache. `this`, except for a composite, which routes to its backend.
454
+ */
455
+ catalogContentsOwner(_attachOpaqueData: AttachOpaqueData): CatalogInterface;
351
456
  transactionBegin(attachOpaqueData: AttachOpaqueData): Awaitable<Uint8Array | null>;
352
457
  transactionCommit(attachOpaqueData: AttachOpaqueData, transactionOpaqueData: TransactionOpaqueData): Awaitable<void>;
353
458
  transactionRollback(attachOpaqueData: AttachOpaqueData, transactionOpaqueData: TransactionOpaqueData): Awaitable<void>;
@@ -2,7 +2,43 @@ import { CatalogInterface, type AttachOpaqueData, type TransactionOpaqueData, ty
2
2
  import { type CatalogDescriptor } from "./descriptors.js";
3
3
  import type { FunctionRegistry } from "../functions/registry.js";
4
4
  import { type AttachOptionSpec } from "./attach-option.js";
5
+ /**
6
+ * The fixed attach id ReadOnlyCatalogInterface writes into its items (same
7
+ * bytes as vgi-python's `ReadOnlyCatalogInterface._FIXED_ATTACH_ID`). Shared:
8
+ * treat it as read-only (a typed array cannot be frozen).
9
+ */
10
+ export declare const READ_ONLY_ITEM_ATTACH_ID: AttachOpaqueData;
5
11
  export declare class ReadOnlyCatalogInterface extends CatalogInterface {
12
+ /**
13
+ * Advertise `supports_catalog_contents` on attach, letting the client load
14
+ * the whole catalog with one `catalog_contents` call instead of
15
+ * `catalog_schemas` plus a `catalog_schema_contents_*` call per schema and
16
+ * kind. True by default — a descriptor catalog is static. Set it to false
17
+ * (before attach) to keep the client on the per-schema RPCs.
18
+ */
19
+ supportsCatalogContents: boolean;
20
+ /** A descriptor catalog's version never changes (`catalog_version_frozen`). */
21
+ catalogVersionFrozen: boolean;
22
+ /**
23
+ * The catalog items (`SchemaInfo.attach_opaque_data`, and the attach value
24
+ * handed to a table function's bind when resolving a table's columns) carry
25
+ * one fixed id, `READ_ONLY_ITEM_ATTACH_ID`, not the per-ATTACH id -- as
26
+ * vgi-python's ReadOnlyCatalogInterface does. So the items are the same for
27
+ * every attach and caller, and the worker caches the `catalog_contents`
28
+ * response (see `catalogContentsCacheable`).
29
+ *
30
+ * `attach()` still mints a random per-ATTACH `attach_opaque_data` (sealed
31
+ * into the per-attach envelope the client holds), and function calls still
32
+ * receive it, so per-attach scoping in functions (the accumulate example) is
33
+ * unchanged.
34
+ *
35
+ * Set true (or pass `{ perAttachItemIds: true }` to the constructor) for a
36
+ * catalog whose items must echo the per-attach id. Its contents then depend
37
+ * on the attach, so the catalog_contents cache is off for it.
38
+ */
39
+ perAttachItemIds: boolean;
40
+ /** Static contents: attach-independent unless `perAttachItemIds` is set. */
41
+ catalogContentsAttachIndependent: boolean;
6
42
  private _descriptor;
7
43
  private _registry;
8
44
  private _attachments;
@@ -18,7 +54,9 @@ export declare class ReadOnlyCatalogInterface extends CatalogInterface {
18
54
  private _functionListings;
19
55
  /** One frozen FunctionInfo per (declaring schema, function), shared by listings and `global_functions`. */
20
56
  private _functionInfos;
21
- constructor(descriptor: CatalogDescriptor, registry: FunctionRegistry);
57
+ constructor(descriptor: CatalogDescriptor, registry: FunctionRegistry, options?: {
58
+ perAttachItemIds?: boolean;
59
+ });
22
60
  /**
23
61
  * Which schema declares `fn`, by object identity, or null if no schema does.
24
62
  *
@@ -51,6 +89,12 @@ export declare class ReadOnlyCatalogInterface extends CatalogInterface {
51
89
  attach(name: string, options?: Record<string, unknown>, dataVersionSpec?: string | null, implementationVersion?: string | null): CatalogAttachResult | Promise<CatalogAttachResult>;
52
90
  detach(attachOpaqueData: AttachOpaqueData): void;
53
91
  version(attachOpaqueData: AttachOpaqueData, transactionOpaqueData?: TransactionOpaqueData): number | Promise<number>;
92
+ /**
93
+ * The attach value written into this catalog's items: the fixed
94
+ * `READ_ONLY_ITEM_ATTACH_ID`, or the caller's attach with `perAttachItemIds`.
95
+ */
96
+ protected itemAttachId(attachOpaqueData: AttachOpaqueData): AttachOpaqueData;
97
+ catalogContentsCacheable(): boolean;
54
98
  schemas(attachOpaqueData: AttachOpaqueData, transactionOpaqueData?: TransactionOpaqueData): SchemaInfo[];
55
99
  private _estimatedObjectCount;
56
100
  schemaGet(attachOpaqueData: AttachOpaqueData, path: string[], transactionOpaqueData?: TransactionOpaqueData): SchemaInfo | null;
@@ -1,6 +1,6 @@
1
1
  import { type VgiBatch } from "../arrow/index.js";
2
2
  import type { RpcClient } from "@query-farm/vgi-rpc";
3
- import { type SchemaInfo, type TableInfo, type ViewInfo, type FunctionInfo, type MacroInfo, type MacroType, type CatalogAttachResult, type AttachOpaqueData, type TransactionOpaqueData, type CatalogInfo, type ScanFunctionResult } from "../catalog/interface.js";
3
+ import { type SchemaInfo, type TableInfo, type ViewInfo, type FunctionInfo, type MacroInfo, type MacroType, type CatalogAttachResult, type SchemaContentsInfo, type AttachOpaqueData, type TransactionOpaqueData, type CatalogInfo, type ScanFunctionResult } from "../catalog/interface.js";
4
4
  import { VgiClientError } from "./errors.js";
5
5
  export { VgiClientError };
6
6
  import type { VgiClientOptions, TableFunctionOptions, ScalarFunctionOptions, TableInOutFunctionOptions, OnCreateConflict, CatalogFunctionType, CatalogMacroType, CatalogAttachOptions, AttachOptionValue } from "./types.js";
@@ -92,6 +92,24 @@ export declare class VgiClient {
92
92
  * ```
93
93
  */
94
94
  catalogAttach(name: string, opts?: CatalogAttachOptions): Promise<CatalogAttachResult>;
95
+ /**
96
+ * Load the whole catalog in one `catalog_contents` call: every schema (parents
97
+ * before children) with all of its tables, views, functions, macros and
98
+ * indexes. Only call it when the attach result set
99
+ * `supports_catalog_contents`; it is the committed catalog at
100
+ * `catalog_version` (no transaction).
101
+ *
102
+ * Pass the `etag` of a snapshot you hold as `ifNoneMatch`: when it is still
103
+ * current the worker answers `not_modified` with no schemas. `etag` null
104
+ * means the worker does not revalidate. Each schema's wire `path` is checked
105
+ * against its decoded `SchemaInfo.path`.
106
+ */
107
+ catalogContents(attachOpaqueData: AttachOpaqueData, ifNoneMatch?: string | null): Promise<{
108
+ catalog_version: number;
109
+ etag: string | null;
110
+ not_modified: boolean;
111
+ schemas: SchemaContentsInfo[];
112
+ }>;
95
113
  /** Detach a previously-attached catalog. */
96
114
  catalogDetach(attachOpaqueData: AttachOpaqueData): Promise<void>;
97
115
  /**
@@ -2284,7 +2284,84 @@ function normalizeSchemaPath(path) {
2284
2284
 
2285
2285
  // src/codec/asd.ts
2286
2286
  function encodeASD(schema, obj) {
2287
- return serializeBatch(batchFromRows([obj], schema));
2287
+ return serializeBatch(batchFromRows([sortMapsForSchema(schema, obj)], schema));
2288
+ }
2289
+ function compareCodePoints(a, b) {
2290
+ const n = Math.min(a.length, b.length);
2291
+ for (let i = 0;i < n; i++) {
2292
+ const x = a.charCodeAt(i);
2293
+ const y = b.charCodeAt(i);
2294
+ if (x === y)
2295
+ continue;
2296
+ const xs = x >= 55296 && x <= 57343;
2297
+ const ys = y >= 55296 && y <= 57343;
2298
+ if (xs !== ys)
2299
+ return xs ? 1 : -1;
2300
+ return x - y;
2301
+ }
2302
+ return a.length - b.length;
2303
+ }
2304
+ function mapPairs(raw) {
2305
+ if (Array.isArray(raw))
2306
+ return raw.map((e) => Array.isArray(e) ? [e[0], e[1]] : [e?.key, e?.value]);
2307
+ if (raw instanceof Map)
2308
+ return Array.from(raw.entries());
2309
+ if (typeof raw[Symbol.iterator] === "function")
2310
+ return Array.from(raw);
2311
+ return Object.entries(raw);
2312
+ }
2313
+ function compareKeys(a, b) {
2314
+ if (typeof a === "string" && typeof b === "string")
2315
+ return compareCodePoints(a, b);
2316
+ if ((typeof a === "number" || typeof a === "bigint") && (typeof b === "number" || typeof b === "bigint")) {
2317
+ return a < b ? -1 : a > b ? 1 : 0;
2318
+ }
2319
+ return compareCodePoints(String(a), String(b));
2320
+ }
2321
+ var HAS_MAP = new WeakMap;
2322
+ function typeHasMap(type) {
2323
+ const cached = HAS_MAP.get(type);
2324
+ if (cached !== undefined)
2325
+ return cached;
2326
+ let has = isMap(type);
2327
+ if (!has && (isList(type) || isStruct(type))) {
2328
+ has = type.children.some((c) => typeHasMap(c.type));
2329
+ }
2330
+ HAS_MAP.set(type, has);
2331
+ return has;
2332
+ }
2333
+ function sortMapsForType(type, value) {
2334
+ if (value == null || !typeHasMap(type))
2335
+ return value;
2336
+ if (isMap(type)) {
2337
+ const entries = type.children[0].type;
2338
+ const valueType = entries.children[1].type;
2339
+ return mapPairs(value).map(([k, v]) => [k, sortMapsForType(valueType, v)]).sort((x, y) => compareKeys(x[0], y[0]));
2340
+ }
2341
+ if (isList(type)) {
2342
+ const child = type.children[0].type;
2343
+ return Array.from(value, (v) => sortMapsForType(child, v));
2344
+ }
2345
+ if (isStruct(type)) {
2346
+ const out = { ...value };
2347
+ for (const cf of type.children)
2348
+ out[cf.name] = sortMapsForType(cf.type, value[cf.name]);
2349
+ return out;
2350
+ }
2351
+ return value;
2352
+ }
2353
+ function sortMapsForSchema(schema, obj) {
2354
+ let out = null;
2355
+ for (const f of schema.fields) {
2356
+ if (!typeHasMap(f.type))
2357
+ continue;
2358
+ const v = obj[f.name];
2359
+ if (v == null)
2360
+ continue;
2361
+ out ??= { ...obj };
2362
+ out[f.name] = sortMapsForType(f.type, v);
2363
+ }
2364
+ return out ?? obj;
2288
2365
  }
2289
2366
  function decodeASD(schema, bytes) {
2290
2367
  const batch = deserializeBatch(bytes);
@@ -2540,6 +2617,18 @@ var ForeignKeyInfoSchema = schema2([
2540
2617
  field2("referenced_table", utf82(), false),
2541
2618
  field2("referenced_schema_path", list2(field2("item", utf82(), true)), false)
2542
2619
  ]);
2620
+ var SchemaContentsSchema = schema2([
2621
+ field2("path", list2(field2("item", utf82(), true)), false),
2622
+ field2("schema", binary2(), false),
2623
+ field2("tables", list2(field2("item", binary2(), true)), false),
2624
+ field2("views", list2(field2("item", binary2(), true)), false),
2625
+ field2("scalar_functions", list2(field2("item", binary2(), true)), false),
2626
+ field2("aggregate_functions", list2(field2("item", binary2(), true)), false),
2627
+ field2("table_functions", list2(field2("item", binary2(), true)), false),
2628
+ field2("scalar_macros", list2(field2("item", binary2(), true)), false),
2629
+ field2("table_macros", list2(field2("item", binary2(), true)), false),
2630
+ field2("indexes", list2(field2("item", binary2(), true)), false)
2631
+ ]);
2543
2632
  var AggregateBindRequestSchema = schema2([
2544
2633
  field2("function_name", utf82(), false),
2545
2634
  field2("arguments", binary2(), false),
@@ -2862,11 +2951,18 @@ var CatalogAttachResultSchema = schema2([
2862
2951
  field2("global_functions", list2(field2("item", binary2(), true)), false),
2863
2952
  field2("global_function_prefix", utf82(), false),
2864
2953
  field2("resolved_data_version", utf82(), true),
2865
- field2("resolved_implementation_version", utf82(), true)
2954
+ field2("resolved_implementation_version", utf82(), true),
2955
+ field2("supports_catalog_contents", bool2(), false)
2866
2956
  ]);
2867
2957
  var CatalogCatalogsResultSchema = schema2([
2868
2958
  field2("items", list2(field2("item", binary2(), true)), false)
2869
2959
  ]);
2960
+ var CatalogContentsResultSchema = schema2([
2961
+ field2("catalog_version", int642(), false),
2962
+ field2("etag", utf82(), true),
2963
+ field2("not_modified", bool2(), false),
2964
+ field2("schemas", list2(field2("item", struct2([field2("path", list2(field2("item", utf82(), true)), false), field2("schema", binary2(), false), field2("tables", list2(field2("item", binary2(), true)), false), field2("views", list2(field2("item", binary2(), true)), false), field2("scalar_functions", list2(field2("item", binary2(), true)), false), field2("aggregate_functions", list2(field2("item", binary2(), true)), false), field2("table_functions", list2(field2("item", binary2(), true)), false), field2("scalar_macros", list2(field2("item", binary2(), true)), false), field2("table_macros", list2(field2("item", binary2(), true)), false), field2("indexes", list2(field2("item", binary2(), true)), false)]), true)), false)
2965
+ ]);
2870
2966
  var CatalogCopyFromFormatsResultSchema = schema2([
2871
2967
  field2("items", list2(field2("item", binary2(), true)), false)
2872
2968
  ]);
@@ -2985,6 +3081,10 @@ var CatalogAttachParamsSchema = schema2([
2985
3081
  field2("request", binary2(), false)
2986
3082
  ]);
2987
3083
  var CatalogCatalogsParamsSchema = schema2([]);
3084
+ var CatalogContentsParamsSchema = schema2([
3085
+ field2("attach_opaque_data", binary2(), false),
3086
+ field2("if_none_match", utf82(), true)
3087
+ ]);
2988
3088
  var CatalogCopyFromFormatsParamsSchema = schema2([
2989
3089
  field2("attach_opaque_data", binary2(), false),
2990
3090
  field2("transaction_opaque_data", binary2(), true)
@@ -3315,10 +3415,14 @@ var TableFunctionStatisticsParamsSchema = schema2([
3315
3415
  field2("request", binary2(), false)
3316
3416
  ]);
3317
3417
 
3418
+ // src/generated/vgi-protocol-types.ts
3419
+ var decodeCatalogContentsResponse = (b) => decodeASD(CatalogContentsResultSchema, b);
3420
+
3318
3421
  // src/generated/vgi-client.ts
3319
3422
  var decodeCatalogInfo = (b) => decodeASD(CatalogInfoSchema, b);
3320
3423
  var encodeFunctionInfo = (v) => encodeASD(FunctionInfoSchema, v);
3321
3424
  var decodeFunctionInfo = (b) => decodeASD(FunctionInfoSchema, b);
3425
+ var decodeIndexInfo = (b) => decodeASD(IndexInfoSchema, b);
3322
3426
  var encodeMacroInfo = (v) => encodeASD(MacroInfoSchema, v);
3323
3427
  var decodeMacroInfo = (b) => decodeASD(MacroInfoSchema, b);
3324
3428
  var encodeSchemaInfo = (v) => encodeASD(SchemaInfoSchema, v);
@@ -3833,7 +3937,40 @@ class VgiClient {
3833
3937
  global_functions: inner.global_functions ? (Array.isArray(inner.global_functions) ? inner.global_functions : [...inner.global_functions]).map(toUint8Array) : [],
3834
3938
  global_function_prefix: inner.global_function_prefix ?? "",
3835
3939
  resolved_data_version: inner.resolved_data_version ?? null,
3836
- resolved_implementation_version: inner.resolved_implementation_version ?? null
3940
+ resolved_implementation_version: inner.resolved_implementation_version ?? null,
3941
+ supports_catalog_contents: inner.supports_catalog_contents ?? false
3942
+ };
3943
+ }
3944
+ async catalogContents(attachOpaqueData, ifNoneMatch = null) {
3945
+ const result = await this.rpc.call("catalog_contents", {
3946
+ attach_opaque_data: attachOpaqueData,
3947
+ if_none_match: ifNoneMatch
3948
+ });
3949
+ if (!result)
3950
+ throw new VgiClientError("catalog_contents returned null");
3951
+ const response = decodeCatalogContentsResponse(toUint8Array(result.result));
3952
+ const schemas = response.schemas.map((c) => {
3953
+ const schema = decodeSchemaInfo(c.schema);
3954
+ if (JSON.stringify(c.path) !== JSON.stringify(schema.path)) {
3955
+ throw new VgiClientError(`catalog_contents: SchemaContents.path ${JSON.stringify(c.path)} != SchemaInfo.path ${JSON.stringify(schema.path)}`);
3956
+ }
3957
+ return {
3958
+ schema,
3959
+ tables: c.tables.map(decodeTableInfo),
3960
+ views: c.views.map(decodeViewInfo),
3961
+ scalar_functions: c.scalar_functions.map(decodeFunctionInfo),
3962
+ aggregate_functions: c.aggregate_functions.map(decodeFunctionInfo),
3963
+ table_functions: c.table_functions.map(decodeFunctionInfo),
3964
+ scalar_macros: c.scalar_macros.map(decodeMacroInfo),
3965
+ table_macros: c.table_macros.map(decodeMacroInfo),
3966
+ indexes: c.indexes.map(decodeIndexInfo)
3967
+ };
3968
+ });
3969
+ return {
3970
+ catalog_version: Number(response.catalog_version ?? 0),
3971
+ etag: response.etag ?? null,
3972
+ not_modified: Boolean(response.not_modified),
3973
+ schemas
3837
3974
  };
3838
3975
  }
3839
3976
  async catalogDetach(attachOpaqueData) {
@@ -4412,4 +4549,4 @@ export {
4412
4549
  typeSignature
4413
4550
  };
4414
4551
 
4415
- //# debugId=8BECE39CB65F6AD764756E2164756E21
4552
+ //# debugId=AD4326E5920F408164756E2164756E21