@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.
Files changed (50) 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 +49 -2
  7. package/dist/client/types.d.ts +56 -1
  8. package/dist/client-entry.js +239 -11
  9. package/dist/client-entry.js.map +9 -8
  10. package/dist/codec/asd.d.ts +8 -0
  11. package/dist/functions/copy-to.d.ts +1 -1
  12. package/dist/generated/vgi-client.d.ts +19 -0
  13. package/dist/generated/vgi-protocol-schemas.d.ts +3 -0
  14. package/dist/generated/vgi-protocol-types.d.ts +26 -0
  15. package/dist/http/fetch.d.ts +2 -1
  16. package/dist/index.core.d.ts +6 -3
  17. package/dist/index.js +672 -43
  18. package/dist/index.js.map +21 -17
  19. package/dist/protocol/handlers/catalog/contents.d.ts +27 -0
  20. package/dist/rpc-server.d.ts +97 -0
  21. package/dist/serve-entry.d.ts +2 -1
  22. package/dist/serve-entry.js +405 -25
  23. package/dist/serve-entry.js.map +18 -14
  24. package/dist/worker-cf-entry.d.ts +3 -0
  25. package/dist/worker-cf-entry.js +528 -31
  26. package/dist/worker-cf-entry.js.map +20 -16
  27. package/dist/worker.d.ts +9 -1
  28. package/package.json +3 -3
  29. package/src/catalog/composite.ts +81 -0
  30. package/src/catalog/contents-digest.ts +78 -0
  31. package/src/catalog/interface.ts +167 -5
  32. package/src/catalog/read-only.ts +68 -6
  33. package/src/client/client.ts +201 -0
  34. package/src/client/types.ts +58 -1
  35. package/src/codec/asd.ts +108 -9
  36. package/src/functions/copy-to.ts +1 -1
  37. package/src/generated/vgi-client.ts +22 -1
  38. package/src/generated/vgi-protocol-schemas.ts +30 -1
  39. package/src/generated/vgi-protocol-types.ts +126 -0
  40. package/src/http/fetch.ts +10 -2
  41. package/src/index.core.ts +23 -2
  42. package/src/protocol/dispatch.ts +1 -1
  43. package/src/protocol/handlers/catalog/admin.ts +35 -26
  44. package/src/protocol/handlers/catalog/contents.ts +162 -0
  45. package/src/protocol/handlers/catalog/macro.ts +2 -1
  46. package/src/protocol/handlers/catalog/shared.ts +12 -1
  47. package/src/rpc-server.ts +273 -0
  48. package/src/serve-entry.ts +7 -1
  49. package/src/worker-cf-entry.ts +8 -0
  50. 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,9 +1,9 @@
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 IndexInfo, type AttachOpaqueData, type TransactionOpaqueData, type CatalogInfo, type ScanFunctionResult } from "../catalog/interface.js";
4
4
  import { VgiClientError } from "./errors.js";
5
5
  export { VgiClientError };
6
- import type { VgiClientOptions, TableFunctionOptions, ScalarFunctionOptions, TableInOutFunctionOptions, OnCreateConflict, CatalogFunctionType, CatalogMacroType, CatalogAttachOptions, AttachOptionValue } from "./types.js";
6
+ import type { VgiClientOptions, TableFunctionOptions, ScalarFunctionOptions, TableInOutFunctionOptions, OnCreateConflict, CatalogFunctionType, CatalogMacroType, CatalogAttachOptions, AttachOptionValue, CatalogSnapshot, LoadCatalogOptions } from "./types.js";
7
7
  /**
8
8
  * High-level client for calling VGI worker functions and catalog API.
9
9
  *
@@ -92,6 +92,51 @@ 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
+ * This is the raw RPC. To enumerate a catalog, use `loadCatalog`, which
108
+ * checks the capability, revalidates and falls back to the per-schema RPCs.
109
+ */
110
+ catalogContents(attachOpaqueData: AttachOpaqueData, ifNoneMatch?: string | null): Promise<{
111
+ catalog_version: number;
112
+ etag: string | null;
113
+ not_modified: boolean;
114
+ schemas: SchemaContentsInfo[];
115
+ }>;
116
+ /**
117
+ * Enumerate the whole catalog: every schema with all of its tables, views,
118
+ * scalar/aggregate/table functions, scalar/table macros and indexes.
119
+ *
120
+ * Honors the attach result's `supports_catalog_contents` the way the DuckDB
121
+ * extension does:
122
+ *
123
+ * - advertised (and `useCatalogContents` not false, no transaction): one
124
+ * `catalog_contents` call. With `previous.etag` it is conditional
125
+ * (`if_none_match`); `not_modified` returns `previous`'s contents.
126
+ * - not advertised: `catalog_schemas` + per-schema
127
+ * `catalog_schema_contents_*` calls -- `catalog_contents` is never sent.
128
+ * Kinds whose `estimated_object_count` is exactly 0 are skipped.
129
+ * - advertised but the call fails (or answers malformed, e.g. a
130
+ * `SchemaContents.path` that disagrees with its `SchemaInfo.path`): falls
131
+ * back to the per-schema calls and reports the failure in
132
+ * `fallback_error`.
133
+ *
134
+ * A full answer older than `previous` (a lagging replica) is retried once,
135
+ * then the per-schema path is used.
136
+ */
137
+ loadCatalog(attach: Pick<CatalogAttachResult, "attach_opaque_data" | "supports_catalog_contents">, opts?: LoadCatalogOptions): Promise<CatalogSnapshot>;
138
+ /** `loadCatalog` without `catalog_contents`: catalog_schemas + per-schema calls. */
139
+ private _loadCatalogPerSchema;
95
140
  /** Detach a previously-attached catalog. */
96
141
  catalogDetach(attachOpaqueData: AttachOpaqueData): Promise<void>;
97
142
  /**
@@ -135,6 +180,8 @@ export declare class VgiClient {
135
180
  schemaContentsViews(attachOpaqueData: AttachOpaqueData, path: string[] | string, transactionOpaqueData?: TransactionOpaqueData): Promise<ViewInfo[]>;
136
181
  /** List functions in a schema, filtered by type. */
137
182
  schemaContentsFunctions(attachOpaqueData: AttachOpaqueData, path: string[] | string, type: CatalogFunctionType, transactionOpaqueData?: TransactionOpaqueData): Promise<FunctionInfo[]>;
183
+ /** List indexes in a schema. */
184
+ schemaContentsIndexes(attachOpaqueData: AttachOpaqueData, path: string[] | string, transactionOpaqueData?: TransactionOpaqueData): Promise<IndexInfo[]>;
138
185
  /** Get a table by name, or null if not found. */
139
186
  tableGet(attachOpaqueData: AttachOpaqueData, schemaPath: string[] | string, name: string, transactionOpaqueData?: TransactionOpaqueData): Promise<TableInfo | null>;
140
187
  /** Create a new table. */
@@ -2,6 +2,7 @@ import type { VgiBatch } from "../arrow/index.js";
2
2
  import type { Arguments } from "../arguments/arguments.js";
3
3
  import type { AttachOptionValue } from "../catalog/attach-options.js";
4
4
  import type { BindResponse } from "../protocol/types.js";
5
+ import type { SchemaContentsInfo, TransactionOpaqueData } from "../catalog/interface.js";
5
6
  import { OrderByDirection, OrderByNullOrder } from "../protocol/types.js";
6
7
  export type { AttachOptionValue };
7
8
  export { OrderByDirection, OrderByNullOrder };
@@ -46,7 +47,7 @@ export interface CatalogAttachOptions {
46
47
  /** Conflict resolution strategy for create operations. */
47
48
  export type OnCreateConflict = "error" | "ignore" | "replace";
48
49
  /** DuckDB catalog function type filter (sent as uppercase wire values). */
49
- export type CatalogFunctionType = "SCALAR_FUNCTION" | "TABLE_FUNCTION";
50
+ export type CatalogFunctionType = "SCALAR_FUNCTION" | "AGGREGATE_FUNCTION" | "TABLE_FUNCTION";
50
51
  /** Macro type filter for schema contents listing (uppercase wire values). */
51
52
  export type CatalogMacroType = "SCALAR_MACRO" | "TABLE_MACRO";
52
53
  /** Options for constructing a VgiClient. */
@@ -136,3 +137,57 @@ export interface TableInOutFunctionOptions {
136
137
  */
137
138
  hasFinalize?: boolean;
138
139
  }
140
+ /**
141
+ * A whole-catalog snapshot returned by `VgiClient.loadCatalog`: every schema
142
+ * with all of its tables, views, functions, macros and indexes.
143
+ *
144
+ * Hold on to it and pass it back as `LoadCatalogOptions.previous` to
145
+ * revalidate: when it carries an `etag` the client sends that as
146
+ * `if_none_match`, and a `not_modified` answer returns this snapshot's
147
+ * contents unchanged.
148
+ */
149
+ export interface CatalogSnapshot {
150
+ /**
151
+ * Version the snapshot was taken at, as reported by `catalog_contents`.
152
+ * `null` when it was assembled from the per-schema RPCs (which carry no
153
+ * version); 0 means the worker does not track versions.
154
+ */
155
+ catalog_version: number | null;
156
+ /** Validator to revalidate with; null = the worker does not revalidate. */
157
+ etag: string | null;
158
+ /**
159
+ * How the contents were obtained: one `catalog_contents` call, or
160
+ * `catalog_schemas` + per-schema `catalog_schema_contents_*` calls.
161
+ */
162
+ source: "catalog_contents" | "per_schema";
163
+ /** True when this load was a `not_modified` revalidation of `previous`. */
164
+ not_modified: boolean;
165
+ /**
166
+ * Set when the catalog advertised `catalog_contents` but the call failed
167
+ * (or returned a malformed answer) and the client fell back to the
168
+ * per-schema RPCs: the failure's message.
169
+ */
170
+ fallback_error: string | null;
171
+ /** One entry per schema, parents before children. */
172
+ schemas: SchemaContentsInfo[];
173
+ }
174
+ /** Options for `VgiClient.loadCatalog`. */
175
+ export interface LoadCatalogOptions {
176
+ /**
177
+ * A snapshot from an earlier load of the same attach. Its `etag` (if any)
178
+ * is sent as `if_none_match`; a `not_modified` answer keeps its contents.
179
+ */
180
+ previous?: CatalogSnapshot | null;
181
+ /**
182
+ * Set false to never call `catalog_contents` (the client-side equivalent of
183
+ * DuckDB's `SET vgi_catalog_contents = false`). Default true: it is used
184
+ * whenever the attach result advertised `supports_catalog_contents`.
185
+ */
186
+ useCatalogContents?: boolean;
187
+ /**
188
+ * Enumerate inside this transaction. `catalog_contents` returns only the
189
+ * committed catalog (it takes no transaction), so a transaction-scoped load
190
+ * always uses the per-schema RPCs.
191
+ */
192
+ transactionOpaqueData?: TransactionOpaqueData;
193
+ }