@query-farm/vgi 0.37.1 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -0
- package/dist/catalog/composite.d.ts +23 -1
- package/dist/catalog/contents-digest.d.ts +25 -0
- package/dist/catalog/interface.d.ts +107 -2
- package/dist/catalog/read-only.d.ts +45 -1
- package/dist/client/client.d.ts +49 -2
- package/dist/client/types.d.ts +56 -1
- package/dist/client-entry.js +239 -11
- package/dist/client-entry.js.map +9 -8
- package/dist/codec/asd.d.ts +8 -0
- package/dist/functions/copy-to.d.ts +1 -1
- package/dist/generated/vgi-client.d.ts +19 -0
- package/dist/generated/vgi-protocol-schemas.d.ts +3 -0
- package/dist/generated/vgi-protocol-types.d.ts +26 -0
- package/dist/http/fetch.d.ts +2 -1
- package/dist/index.core.d.ts +6 -3
- package/dist/index.js +672 -43
- package/dist/index.js.map +21 -17
- package/dist/protocol/handlers/catalog/contents.d.ts +27 -0
- package/dist/rpc-server.d.ts +97 -0
- package/dist/serve-entry.d.ts +2 -1
- package/dist/serve-entry.js +405 -25
- package/dist/serve-entry.js.map +18 -14
- package/dist/worker-cf-entry.d.ts +3 -0
- package/dist/worker-cf-entry.js +528 -31
- package/dist/worker-cf-entry.js.map +20 -16
- package/dist/worker.d.ts +9 -1
- package/package.json +3 -3
- package/src/catalog/composite.ts +81 -0
- package/src/catalog/contents-digest.ts +78 -0
- package/src/catalog/interface.ts +167 -5
- package/src/catalog/read-only.ts +68 -6
- package/src/client/client.ts +201 -0
- package/src/client/types.ts +58 -1
- package/src/codec/asd.ts +108 -9
- package/src/functions/copy-to.ts +1 -1
- package/src/generated/vgi-client.ts +22 -1
- package/src/generated/vgi-protocol-schemas.ts +30 -1
- package/src/generated/vgi-protocol-types.ts +126 -0
- package/src/http/fetch.ts +10 -2
- package/src/index.core.ts +23 -2
- package/src/protocol/dispatch.ts +1 -1
- package/src/protocol/handlers/catalog/admin.ts +35 -26
- package/src/protocol/handlers/catalog/contents.ts +162 -0
- package/src/protocol/handlers/catalog/macro.ts +2 -1
- package/src/protocol/handlers/catalog/shared.ts +12 -1
- package/src/rpc-server.ts +273 -0
- package/src/serve-entry.ts +7 -1
- package/src/worker-cf-entry.ts +8 -0
- package/src/worker.ts +24 -6
package/src/client/client.ts
CHANGED
|
@@ -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,
|
package/src/client/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
191
|
+
put(entry[0], entry[1]);
|
|
89
192
|
} else if (entry && typeof entry === "object") {
|
|
90
|
-
|
|
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
|
}
|
package/src/functions/copy-to.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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),
|