@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
@@ -40,6 +40,9 @@ import {
40
40
  decodeMacroInfo,
41
41
  type MacroType,
42
42
  type CatalogAttachResult,
43
+ type SchemaContentsInfo,
44
+ decodeIndexInfo,
45
+ type IndexInfo,
43
46
  type AttachOpaqueData,
44
47
  type TransactionOpaqueData,
45
48
  decodeCatalogInfo,
@@ -54,6 +57,7 @@ import { VgiClientError, wrapRpcWithErrorEnrichment } from "./errors.js";
54
57
  import { normalizeSchemaPath } from "../schema-path.js";
55
58
  import { encodeASD } from "../codec/asd.js";
56
59
  import { ClientCapabilitiesSchema, MacroCreateRequestSchema } from "../generated/vgi-protocol-schemas.js";
60
+ import { decodeCatalogContentsResponse } from "../generated/vgi-protocol-types.js";
57
61
  import { deserializeInfoList, deserializeTags, toAsyncIterator } from "./helpers.js";
58
62
 
59
63
  export { VgiClientError };
@@ -69,6 +73,8 @@ import type {
69
73
  OrderByPushdown,
70
74
  TablesamplePushdown,
71
75
  AttachOptionValue,
76
+ CatalogSnapshot,
77
+ LoadCatalogOptions,
72
78
  } from "./types.js";
73
79
 
74
80
  /**
@@ -678,6 +684,185 @@ export class VgiClient {
678
684
  global_function_prefix: inner.global_function_prefix ?? "",
679
685
  resolved_data_version: inner.resolved_data_version ?? null,
680
686
  resolved_implementation_version: inner.resolved_implementation_version ?? null,
687
+ supports_catalog_contents: inner.supports_catalog_contents ?? false,
688
+ };
689
+ }
690
+
691
+ /**
692
+ * Load the whole catalog in one `catalog_contents` call: every schema (parents
693
+ * before children) with all of its tables, views, functions, macros and
694
+ * indexes. Only call it when the attach result set
695
+ * `supports_catalog_contents`; it is the committed catalog at
696
+ * `catalog_version` (no transaction).
697
+ *
698
+ * Pass the `etag` of a snapshot you hold as `ifNoneMatch`: when it is still
699
+ * current the worker answers `not_modified` with no schemas. `etag` null
700
+ * means the worker does not revalidate. Each schema's wire `path` is checked
701
+ * against its decoded `SchemaInfo.path`.
702
+ *
703
+ * This is the raw RPC. To enumerate a catalog, use `loadCatalog`, which
704
+ * checks the capability, revalidates and falls back to the per-schema RPCs.
705
+ */
706
+ async catalogContents(
707
+ attachOpaqueData: AttachOpaqueData,
708
+ ifNoneMatch: string | null = null,
709
+ ): Promise<{ catalog_version: number; etag: string | null; not_modified: boolean; schemas: SchemaContentsInfo[] }> {
710
+ const result = await this.rpc.call("catalog_contents", {
711
+ attach_opaque_data: attachOpaqueData,
712
+ if_none_match: ifNoneMatch,
713
+ });
714
+ if (!result) throw new VgiClientError("catalog_contents returned null");
715
+ const response = decodeCatalogContentsResponse(toUint8Array(result.result));
716
+ const schemas = response.schemas.map((c) => {
717
+ const schema = decodeSchemaInfo(c.schema);
718
+ if (JSON.stringify(c.path) !== JSON.stringify(schema.path)) {
719
+ throw new VgiClientError(
720
+ `catalog_contents: SchemaContents.path ${JSON.stringify(c.path)} != SchemaInfo.path ${JSON.stringify(schema.path)}`,
721
+ );
722
+ }
723
+ return {
724
+ schema,
725
+ tables: c.tables.map(decodeTableInfo),
726
+ views: c.views.map(decodeViewInfo),
727
+ scalar_functions: c.scalar_functions.map(decodeFunctionInfo),
728
+ aggregate_functions: c.aggregate_functions.map(decodeFunctionInfo),
729
+ table_functions: c.table_functions.map(decodeFunctionInfo),
730
+ scalar_macros: c.scalar_macros.map(decodeMacroInfo),
731
+ table_macros: c.table_macros.map(decodeMacroInfo),
732
+ indexes: c.indexes.map(decodeIndexInfo),
733
+ };
734
+ });
735
+ return {
736
+ catalog_version: Number(response.catalog_version ?? 0),
737
+ etag: response.etag ?? null,
738
+ not_modified: Boolean(response.not_modified),
739
+ schemas,
740
+ };
741
+ }
742
+
743
+ /**
744
+ * Enumerate the whole catalog: every schema with all of its tables, views,
745
+ * scalar/aggregate/table functions, scalar/table macros and indexes.
746
+ *
747
+ * Honors the attach result's `supports_catalog_contents` the way the DuckDB
748
+ * extension does:
749
+ *
750
+ * - advertised (and `useCatalogContents` not false, no transaction): one
751
+ * `catalog_contents` call. With `previous.etag` it is conditional
752
+ * (`if_none_match`); `not_modified` returns `previous`'s contents.
753
+ * - not advertised: `catalog_schemas` + per-schema
754
+ * `catalog_schema_contents_*` calls -- `catalog_contents` is never sent.
755
+ * Kinds whose `estimated_object_count` is exactly 0 are skipped.
756
+ * - advertised but the call fails (or answers malformed, e.g. a
757
+ * `SchemaContents.path` that disagrees with its `SchemaInfo.path`): falls
758
+ * back to the per-schema calls and reports the failure in
759
+ * `fallback_error`.
760
+ *
761
+ * A full answer older than `previous` (a lagging replica) is retried once,
762
+ * then the per-schema path is used.
763
+ */
764
+ async loadCatalog(
765
+ attach: Pick<CatalogAttachResult, "attach_opaque_data" | "supports_catalog_contents">,
766
+ opts?: LoadCatalogOptions,
767
+ ): Promise<CatalogSnapshot> {
768
+ const attachOpaqueData = attach.attach_opaque_data;
769
+ const previous = opts?.previous ?? null;
770
+ const useContents =
771
+ attach.supports_catalog_contents === true &&
772
+ opts?.useCatalogContents !== false &&
773
+ opts?.transactionOpaqueData == null;
774
+ if (!useContents) {
775
+ return this._loadCatalogPerSchema(attachOpaqueData, opts?.transactionOpaqueData, null);
776
+ }
777
+
778
+ let fallbackError: string | null = null;
779
+ const ifNoneMatch = previous?.etag ?? null;
780
+ const knownVersion = previous?.catalog_version ?? 0;
781
+ for (let attempt = 0; attempt < 2; attempt++) {
782
+ let response: Awaited<ReturnType<VgiClient["catalogContents"]>>;
783
+ try {
784
+ response = await this.catalogContents(attachOpaqueData, ifNoneMatch);
785
+ } catch (err) {
786
+ fallbackError = err instanceof Error ? err.message : String(err);
787
+ break;
788
+ }
789
+ if (response.not_modified) {
790
+ if (previous == null || ifNoneMatch == null) {
791
+ fallbackError = "catalog_contents answered not_modified to an unconditional request";
792
+ break;
793
+ }
794
+ return {
795
+ ...previous,
796
+ // 0 = unknown: keep the version already known.
797
+ catalog_version: response.catalog_version !== 0 ? response.catalog_version : previous.catalog_version,
798
+ etag: response.etag ?? ifNoneMatch,
799
+ not_modified: true,
800
+ fallback_error: null,
801
+ };
802
+ }
803
+ // Version adoption: a snapshot at least as new as the known version is
804
+ // current; 0 means unknown and is accepted. An older one is retried once.
805
+ if (response.catalog_version !== 0 && knownVersion !== 0 && response.catalog_version < knownVersion) {
806
+ fallbackError = `catalog_contents snapshot version ${response.catalog_version} is older than the known version ${knownVersion}`;
807
+ continue;
808
+ }
809
+ return {
810
+ catalog_version: response.catalog_version,
811
+ etag: response.etag,
812
+ source: "catalog_contents",
813
+ not_modified: false,
814
+ fallback_error: null,
815
+ schemas: response.schemas,
816
+ };
817
+ }
818
+ return this._loadCatalogPerSchema(attachOpaqueData, undefined, fallbackError);
819
+ }
820
+
821
+ /** `loadCatalog` without `catalog_contents`: catalog_schemas + per-schema calls. */
822
+ private async _loadCatalogPerSchema(
823
+ attachOpaqueData: AttachOpaqueData,
824
+ transactionOpaqueData: TransactionOpaqueData | undefined,
825
+ fallbackError: string | null,
826
+ ): Promise<CatalogSnapshot> {
827
+ const schemas = await this.schemas(attachOpaqueData, transactionOpaqueData);
828
+ const out: SchemaContentsInfo[] = [];
829
+ // Sequential on purpose: a stdio RpcClient carries one call at a time.
830
+ for (const schema of schemas) {
831
+ const counts = schema.estimated_object_count ?? {};
832
+ const path = schema.path;
833
+ // Only an explicit 0 is a guarantee; a missing count is unknown. Decoded
834
+ // int64 counts are numbers, or bigints beyond the safe range, so compare
835
+ // numerically.
836
+ const has = (kind: string) => {
837
+ const n = (counts as Record<string, unknown>)[kind];
838
+ return n == null || Number(n) !== 0;
839
+ };
840
+ const t = transactionOpaqueData;
841
+ out.push({
842
+ schema,
843
+ tables: has("table") ? await this.schemaContentsTables(attachOpaqueData, path, t) : [],
844
+ views: has("view") ? await this.schemaContentsViews(attachOpaqueData, path, t) : [],
845
+ scalar_functions: has("scalar_function")
846
+ ? await this.schemaContentsFunctions(attachOpaqueData, path, "SCALAR_FUNCTION", t)
847
+ : [],
848
+ aggregate_functions: has("aggregate_function")
849
+ ? await this.schemaContentsFunctions(attachOpaqueData, path, "AGGREGATE_FUNCTION", t)
850
+ : [],
851
+ table_functions: has("table_function")
852
+ ? await this.schemaContentsFunctions(attachOpaqueData, path, "TABLE_FUNCTION", t)
853
+ : [],
854
+ scalar_macros: has("macro") ? await this.schemaContentsMacros(attachOpaqueData, path, "SCALAR_MACRO", t) : [],
855
+ table_macros: has("macro") ? await this.schemaContentsMacros(attachOpaqueData, path, "TABLE_MACRO", t) : [],
856
+ indexes: has("index") ? await this.schemaContentsIndexes(attachOpaqueData, path, t) : [],
857
+ });
858
+ }
859
+ return {
860
+ catalog_version: null,
861
+ etag: null,
862
+ source: "per_schema",
863
+ not_modified: false,
864
+ fallback_error: fallbackError,
865
+ schemas: out,
681
866
  };
682
867
  }
683
868
 
@@ -902,6 +1087,22 @@ export class VgiClient {
902
1087
  return deserializeInfoList(inner.items, decodeFunctionInfo);
903
1088
  }
904
1089
 
1090
+ /** List indexes in a schema. */
1091
+ async schemaContentsIndexes(
1092
+ attachOpaqueData: AttachOpaqueData,
1093
+ path: string[] | string,
1094
+ transactionOpaqueData?: TransactionOpaqueData,
1095
+ ): Promise<IndexInfo[]> {
1096
+ const result = await this.rpc.call("catalog_schema_contents_indexes", {
1097
+ attach_opaque_data: attachOpaqueData,
1098
+ path: normalizeSchemaPath(path),
1099
+ transaction_opaque_data: transactionOpaqueData ?? null,
1100
+ });
1101
+ if (!result) return [];
1102
+ const inner = unwrapResult(result);
1103
+ return deserializeInfoList(inner.items, decodeIndexInfo);
1104
+ }
1105
+
905
1106
  /** Get a table by name, or null if not found. */
906
1107
  async tableGet(
907
1108
  attachOpaqueData: AttachOpaqueData,
@@ -5,6 +5,7 @@ import type { VgiBatch } from "../arrow/index.js";
5
5
  import type { Arguments } from "../arguments/arguments.js";
6
6
  import type { AttachOptionValue } from "../catalog/attach-options.js";
7
7
  import type { BindResponse } from "../protocol/types.js";
8
+ import type { SchemaContentsInfo, TransactionOpaqueData } from "../catalog/interface.js";
8
9
  import { OrderByDirection, OrderByNullOrder } from "../protocol/types.js";
9
10
 
10
11
  export type { AttachOptionValue };
@@ -56,7 +57,7 @@ export interface CatalogAttachOptions {
56
57
  export type OnCreateConflict = "error" | "ignore" | "replace";
57
58
 
58
59
  /** DuckDB catalog function type filter (sent as uppercase wire values). */
59
- export type CatalogFunctionType = "SCALAR_FUNCTION" | "TABLE_FUNCTION";
60
+ export type CatalogFunctionType = "SCALAR_FUNCTION" | "AGGREGATE_FUNCTION" | "TABLE_FUNCTION";
60
61
 
61
62
  /** Macro type filter for schema contents listing (uppercase wire values). */
62
63
  export type CatalogMacroType = "SCALAR_MACRO" | "TABLE_MACRO";
@@ -151,3 +152,59 @@ export interface TableInOutFunctionOptions {
151
152
  */
152
153
  hasFinalize?: boolean;
153
154
  }
155
+
156
+ /**
157
+ * A whole-catalog snapshot returned by `VgiClient.loadCatalog`: every schema
158
+ * with all of its tables, views, functions, macros and indexes.
159
+ *
160
+ * Hold on to it and pass it back as `LoadCatalogOptions.previous` to
161
+ * revalidate: when it carries an `etag` the client sends that as
162
+ * `if_none_match`, and a `not_modified` answer returns this snapshot's
163
+ * contents unchanged.
164
+ */
165
+ export interface CatalogSnapshot {
166
+ /**
167
+ * Version the snapshot was taken at, as reported by `catalog_contents`.
168
+ * `null` when it was assembled from the per-schema RPCs (which carry no
169
+ * version); 0 means the worker does not track versions.
170
+ */
171
+ catalog_version: number | null;
172
+ /** Validator to revalidate with; null = the worker does not revalidate. */
173
+ etag: string | null;
174
+ /**
175
+ * How the contents were obtained: one `catalog_contents` call, or
176
+ * `catalog_schemas` + per-schema `catalog_schema_contents_*` calls.
177
+ */
178
+ source: "catalog_contents" | "per_schema";
179
+ /** True when this load was a `not_modified` revalidation of `previous`. */
180
+ not_modified: boolean;
181
+ /**
182
+ * Set when the catalog advertised `catalog_contents` but the call failed
183
+ * (or returned a malformed answer) and the client fell back to the
184
+ * per-schema RPCs: the failure's message.
185
+ */
186
+ fallback_error: string | null;
187
+ /** One entry per schema, parents before children. */
188
+ schemas: SchemaContentsInfo[];
189
+ }
190
+
191
+ /** Options for `VgiClient.loadCatalog`. */
192
+ export interface LoadCatalogOptions {
193
+ /**
194
+ * A snapshot from an earlier load of the same attach. Its `etag` (if any)
195
+ * is sent as `if_none_match`; a `not_modified` answer keeps its contents.
196
+ */
197
+ previous?: CatalogSnapshot | null;
198
+ /**
199
+ * Set false to never call `catalog_contents` (the client-side equivalent of
200
+ * DuckDB's `SET vgi_catalog_contents = false`). Default true: it is used
201
+ * whenever the attach result advertised `supports_catalog_contents`.
202
+ */
203
+ useCatalogContents?: boolean;
204
+ /**
205
+ * Enumerate inside this transaction. `catalog_contents` returns only the
206
+ * committed catalog (it takes no transaction), so a transaction-scoped load
207
+ * always uses the per-schema RPCs.
208
+ */
209
+ transactionOpaqueData?: TransactionOpaqueData;
210
+ }
package/src/codec/asd.ts CHANGED
@@ -40,7 +40,102 @@ export function encodeASD(
40
40
  schema: VgiSchema,
41
41
  obj: Record<string, any>,
42
42
  ): Uint8Array {
43
- return serializeBatch(batchFromRows([obj], schema));
43
+ return serializeBatch(batchFromRows([sortMapsForSchema(schema, obj)], schema));
44
+ }
45
+
46
+ // --------------------------------------------------------------------------- //
47
+ // Encode side: deterministic map columns
48
+ // --------------------------------------------------------------------------- //
49
+ //
50
+ // A map value arrives as a JS object (or Map / pair list), whose iteration
51
+ // order is insertion order -- except that integer-like keys ("1", "2") always
52
+ // come first, ascending. Two builds of the same record could therefore encode
53
+ // differently (and a catalog_contents content-hash etag would change with no
54
+ // catalog change). Every map is written in sorted key order instead, matching
55
+ // the Go / Java / C# / Rust SDKs: keys compared by Unicode code point (the
56
+ // same order as comparing their UTF-8 bytes), not by UTF-16 code unit.
57
+
58
+ /** Compare two strings by Unicode code point (= UTF-8 byte order). */
59
+ export function compareCodePoints(a: string, b: string): number {
60
+ const n = Math.min(a.length, b.length);
61
+ for (let i = 0; i < n; i++) {
62
+ const x = a.charCodeAt(i);
63
+ const y = b.charCodeAt(i);
64
+ if (x === y) continue;
65
+ // A surrogate (a code point above U+FFFF) sorts after every BMP code unit,
66
+ // though as a code unit (0xD800-0xDFFF) it sorts below U+E000-U+FFFF.
67
+ const xs = x >= 0xd800 && x <= 0xdfff;
68
+ const ys = y >= 0xd800 && y <= 0xdfff;
69
+ if (xs !== ys) return xs ? 1 : -1;
70
+ return x - y;
71
+ }
72
+ return a.length - b.length;
73
+ }
74
+
75
+ function mapPairs(raw: any): Array<[unknown, unknown]> {
76
+ if (Array.isArray(raw)) return raw.map((e: any) => (Array.isArray(e) ? [e[0], e[1]] : [e?.key, e?.value]));
77
+ if (raw instanceof Map) return Array.from(raw.entries());
78
+ if (typeof raw[Symbol.iterator] === "function") return Array.from(raw as Iterable<[unknown, unknown]>);
79
+ return Object.entries(raw);
80
+ }
81
+
82
+ function compareKeys(a: unknown, b: unknown): number {
83
+ if (typeof a === "string" && typeof b === "string") return compareCodePoints(a, b);
84
+ if ((typeof a === "number" || typeof a === "bigint") && (typeof b === "number" || typeof b === "bigint")) {
85
+ return a < b ? -1 : a > b ? 1 : 0;
86
+ }
87
+ return compareCodePoints(String(a), String(b));
88
+ }
89
+
90
+ /** Whether a type contains a map anywhere (memoized per type object). */
91
+ const HAS_MAP = new WeakMap<object, boolean>();
92
+ function typeHasMap(type: VgiDataType): boolean {
93
+ const cached = HAS_MAP.get(type as object);
94
+ if (cached !== undefined) return cached;
95
+ let has = isMap(type);
96
+ if (!has && (isList(type) || isStruct(type))) {
97
+ has = ((type as any).children as VgiField[]).some((c) => typeHasMap(c.type));
98
+ }
99
+ HAS_MAP.set(type as object, has);
100
+ return has;
101
+ }
102
+
103
+ function sortMapsForType(type: VgiDataType, value: any): any {
104
+ if (value == null || !typeHasMap(type)) return value;
105
+ if (isMap(type)) {
106
+ const entries = (type as any).children[0].type as VgiDataType;
107
+ const valueType = ((entries as any).children as VgiField[])[1].type;
108
+ return mapPairs(value)
109
+ .map(([k, v]): [unknown, unknown] => [k, sortMapsForType(valueType, v)])
110
+ .sort((x, y) => compareKeys(x[0], y[0]));
111
+ }
112
+ if (isList(type)) {
113
+ const child = (type as any).children[0].type as VgiDataType;
114
+ return Array.from(value as Iterable<unknown>, (v) => sortMapsForType(child, v));
115
+ }
116
+ if (isStruct(type)) {
117
+ const out: Record<string, any> = { ...value };
118
+ for (const cf of (type as any).children as VgiField[]) out[cf.name] = sortMapsForType(cf.type, value[cf.name]);
119
+ return out;
120
+ }
121
+ return value;
122
+ }
123
+
124
+ /**
125
+ * `obj` with every map-typed field (at any depth) as a key-sorted pair list.
126
+ * Returns `obj` itself when the schema has no map column. Never mutates `obj`
127
+ * (catalog items may be frozen and shared).
128
+ */
129
+ export function sortMapsForSchema(schema: VgiSchema, obj: Record<string, any>): Record<string, any> {
130
+ let out: Record<string, any> | null = null;
131
+ for (const f of schema.fields) {
132
+ if (!typeHasMap(f.type)) continue;
133
+ const v = obj[f.name];
134
+ if (v == null) continue;
135
+ out ??= { ...obj };
136
+ out[f.name] = sortMapsForType(f.type, v);
137
+ }
138
+ return out ?? obj;
44
139
  }
45
140
 
46
141
  /**
@@ -80,23 +175,27 @@ function normalizeValue(raw: any, type: VgiDataType): any {
80
175
  }
81
176
 
82
177
  if (isMap(type)) {
83
- const out: Record<string, string> = {};
178
+ // Values are decoded with the map's declared value type, like list items
179
+ // and struct children: a map<utf8, int64> yields numbers (bigint beyond
180
+ // the safe range), not strings, and a null value stays null.
181
+ const entries = (type as any).children[0].type as VgiDataType;
182
+ const valueType = ((entries as any).children as VgiField[])[1].type;
183
+ const out: Record<string, any> = {};
184
+ const put = (k: unknown, v: unknown): void => {
185
+ out[String(k)] = normalizeValue(v, valueType);
186
+ };
84
187
  // arrow-js MapRow is iterable of [k,v]; flechette returns [[k,v],...] arrays.
85
188
  if (raw[Symbol.iterator]) {
86
189
  for (const entry of raw) {
87
190
  if (Array.isArray(entry)) {
88
- out[String(entry[0])] = String(entry[1] ?? "");
191
+ put(entry[0], entry[1]);
89
192
  } else if (entry && typeof entry === "object") {
90
- const k = entry.key ?? entry[0];
91
- const v = entry.value ?? entry[1];
92
- out[String(k)] = v == null ? "" : String(v);
193
+ put(entry.key ?? entry[0], entry.value ?? entry[1]);
93
194
  }
94
195
  }
95
196
  } else if (typeof raw === "object") {
96
197
  // Plain-object map (flechette without useMap): Object.entries fallback
97
- for (const [k, v] of Object.entries(raw)) {
98
- out[String(k)] = v == null ? "" : String(v);
99
- }
198
+ for (const [k, v] of Object.entries(raw)) put(k, v);
100
199
  }
101
200
  return out;
102
201
  }
@@ -75,7 +75,7 @@ export interface CopyToOption {
75
75
  * Optional allowed value set, validated worker-side at bind. Mirrors
76
76
  * vgi-python's `Arg(..., choices=[...])`.
77
77
  */
78
- choices?: unknown[];
78
+ choices?: readonly unknown[];
79
79
  /**
80
80
  * Optional inclusive lower bound for numeric options, validated worker-side at
81
81
  * bind. Mirrors vgi-python's `Arg(..., ge=...)`.
@@ -2,7 +2,7 @@
2
2
  // GENERATED by vgi.codegen.ts_client. DO NOT EDIT BY HAND.
3
3
  //
4
4
  // Generator: vgi-gen-ts-client v1
5
- // Content hash: 69731fc31f09
5
+ // Content hash: 5cf87195498c
6
6
  //
7
7
  // To regenerate:
8
8
  // uv run --project ~/Development/vgi-python vgi-gen-ts-client \
@@ -423,12 +423,33 @@ export interface CatalogAttachResult {
423
423
  global_function_prefix: string;
424
424
  resolved_data_version?: string | null;
425
425
  resolved_implementation_version?: string | null;
426
+ supports_catalog_contents: boolean;
426
427
  }
427
428
 
428
429
  export interface CatalogsResponse {
429
430
  items: CatalogInfo[];
430
431
  }
431
432
 
433
+ export interface SchemaContents {
434
+ path: string[];
435
+ schema: Uint8Array;
436
+ tables: Uint8Array[];
437
+ views: Uint8Array[];
438
+ scalar_functions: Uint8Array[];
439
+ aggregate_functions: Uint8Array[];
440
+ table_functions: Uint8Array[];
441
+ scalar_macros: Uint8Array[];
442
+ table_macros: Uint8Array[];
443
+ indexes: Uint8Array[];
444
+ }
445
+
446
+ export interface CatalogContentsResponse {
447
+ catalog_version: number;
448
+ etag?: string | null;
449
+ not_modified: boolean;
450
+ schemas: SchemaContents[];
451
+ }
452
+
432
453
  export interface CopyFromFormatsResponse {
433
454
  items: Uint8Array[];
434
455
  }
@@ -2,7 +2,7 @@
2
2
  // GENERATED by vgi.codegen.ts_schemas. DO NOT EDIT BY HAND.
3
3
  //
4
4
  // Generator: vgi-gen-ts-schemas v2
5
- // Content hash: 668f198950df
5
+ // Content hash: 36a9087555c9
6
6
  //
7
7
  // To regenerate:
8
8
  // uv run --project ~/Development/vgi-python vgi-gen-ts-schemas \
@@ -228,6 +228,20 @@ export const ForeignKeyInfoSchema = schema([
228
228
  field("referenced_schema_path", list(field("item", utf8(), true)), false),
229
229
  ]);
230
230
 
231
+ // Origin: SchemaContents
232
+ export const SchemaContentsSchema = schema([
233
+ field("path", list(field("item", utf8(), true)), false),
234
+ field("schema", binary(), false),
235
+ field("tables", list(field("item", binary(), true)), false),
236
+ field("views", list(field("item", binary(), true)), false),
237
+ field("scalar_functions", list(field("item", binary(), true)), false),
238
+ field("aggregate_functions", list(field("item", binary(), true)), false),
239
+ field("table_functions", list(field("item", binary(), true)), false),
240
+ field("scalar_macros", list(field("item", binary(), true)), false),
241
+ field("table_macros", list(field("item", binary(), true)), false),
242
+ field("indexes", list(field("item", binary(), true)), false),
243
+ ]);
244
+
231
245
  // Origin: AggregateBindRequest
232
246
  export const AggregateBindRequestSchema = schema([
233
247
  field("function_name", utf8(), false),
@@ -638,6 +652,7 @@ export const CatalogAttachResultSchema = schema([
638
652
  field("global_function_prefix", utf8(), false),
639
653
  field("resolved_data_version", utf8(), true),
640
654
  field("resolved_implementation_version", utf8(), true),
655
+ field("supports_catalog_contents", bool(), false),
641
656
  ]);
642
657
 
643
658
  // Origin: method 'catalog_catalogs' result
@@ -645,6 +660,14 @@ export const CatalogCatalogsResultSchema = schema([
645
660
  field("items", list(field("item", binary(), true)), false),
646
661
  ]);
647
662
 
663
+ // Origin: method 'catalog_contents' result
664
+ export const CatalogContentsResultSchema = schema([
665
+ field("catalog_version", int64(), false),
666
+ field("etag", utf8(), true),
667
+ field("not_modified", bool(), false),
668
+ field("schemas", list(field("item", struct([field("path", list(field("item", utf8(), true)), false), field("schema", binary(), false), field("tables", list(field("item", binary(), true)), false), field("views", list(field("item", binary(), true)), false), field("scalar_functions", list(field("item", binary(), true)), false), field("aggregate_functions", list(field("item", binary(), true)), false), field("table_functions", list(field("item", binary(), true)), false), field("scalar_macros", list(field("item", binary(), true)), false), field("table_macros", list(field("item", binary(), true)), false), field("indexes", list(field("item", binary(), true)), false)]), true)), false),
669
+ ]);
670
+
648
671
  // Origin: method 'catalog_copy_from_formats' result
649
672
  export const CatalogCopyFromFormatsResultSchema = schema([
650
673
  field("items", list(field("item", binary(), true)), false),
@@ -833,6 +856,12 @@ export const CatalogAttachParamsSchema = schema([
833
856
  // Origin: method 'catalog_catalogs' params
834
857
  export const CatalogCatalogsParamsSchema = schema([]);
835
858
 
859
+ // Origin: method 'catalog_contents' params
860
+ export const CatalogContentsParamsSchema = schema([
861
+ field("attach_opaque_data", binary(), false),
862
+ field("if_none_match", utf8(), true),
863
+ ]);
864
+
836
865
  // Origin: method 'catalog_copy_from_formats' params
837
866
  export const CatalogCopyFromFormatsParamsSchema = schema([
838
867
  field("attach_opaque_data", binary(), false),