@query-farm/vgi 0.12.0 → 0.13.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.
@@ -0,0 +1,62 @@
1
+ import type { CatalogInterface } from "../catalog/interface.js";
2
+ export declare const LANDING_SCHEMA_VERSION = 1;
3
+ export declare const CUPOLA_BASE = "https://cupola.query-farm.services";
4
+ /**
5
+ * Producer for the VGI landing surface, passed to `createHttpHandler`'s
6
+ * `landingDescribe` option. Declared locally (structurally identical to
7
+ * `@query-farm/vgi-rpc`'s `LandingDescribeProvider`) so this package does not
8
+ * hard-couple to a specific vgi-rpc version.
9
+ */
10
+ export interface LandingDescribeProvider {
11
+ describe(ctx: {
12
+ serverId: string;
13
+ oauth: boolean;
14
+ }): unknown | Promise<unknown>;
15
+ columns(catalog: string, schema: string, table: string): unknown | null | Promise<unknown | null>;
16
+ }
17
+ /** Identity of the worker software surfaced in `describe.json` → `worker`. */
18
+ export interface WorkerDescribeInfo {
19
+ /** Worker name (e.g. class or catalog name). */
20
+ name: string;
21
+ /** One-line worker description. */
22
+ doc?: string;
23
+ /** Worker software version. */
24
+ version?: string;
25
+ }
26
+ interface ColumnJson {
27
+ name: string;
28
+ type: string;
29
+ comment?: string;
30
+ }
31
+ /**
32
+ * Build the full `describe.json` document for a worker's catalog interface.
33
+ *
34
+ * `oauth` and `serverId` are supplied by the HTTP server (they depend on runtime
35
+ * config, not the catalog).
36
+ */
37
+ export declare function buildDescribeJson(iface: CatalogInterface, worker: WorkerDescribeInfo, opts?: {
38
+ oauth?: boolean;
39
+ serverId?: string;
40
+ cupolaBase?: string;
41
+ }): Promise<Record<string, unknown>>;
42
+ /**
43
+ * Build the lazy per-object column payload for one table or view.
44
+ *
45
+ * Returns `{ columns: [{ name, type, comment? }] }`, or `null` when the object
46
+ * can't be found. Tables deserialize their Arrow column schema; views expose
47
+ * their declared column comments (types are only known after binding the SQL,
48
+ * which the worker does not do here).
49
+ */
50
+ export declare function buildColumnsJson(iface: CatalogInterface, catalog: string, schema: string, table: string): Promise<{
51
+ columns: ColumnJson[];
52
+ } | null>;
53
+ /**
54
+ * Build a {@link LandingDescribeProvider} to pass to `createHttpHandler` from
55
+ * `@query-farm/vgi-rpc`. Wires the worker's catalog interface into the
56
+ * standardized landing surface (`GET {prefix}/describe.json` and the lazy
57
+ * per-object column endpoint).
58
+ */
59
+ export declare function createLandingDescribe(iface: CatalogInterface, worker: WorkerDescribeInfo, opts?: {
60
+ cupolaBase?: string;
61
+ }): LandingDescribeProvider;
62
+ export {};
@@ -23,6 +23,7 @@ export { metadatasToArrow, arrowToMetadatas } from "./metadata/serialize.js";
23
23
  export { PushdownFilters, FilteringOutputCollector, deserializeFilters, buildJoinKeysLookup, ComparisonOp, type Filter, type ConstantFilter, type IsNullFilter, type IsNotNullFilter, type InFilter, type AndFilter, type OrFilter, type StructFilter, formatPushedFilters, reprPushedFilters, } from "./filter-pushdown/index.js";
24
24
  export { arrowStateSerializer, EXCHANGE_STATE_SCHEMA, serializeUserState, deserializeUserState } from "./protocol/state-serializer.js";
25
25
  export { buildVgiProtocol, type ProtocolConfig } from "./protocol/dispatch.js";
26
+ export { buildColumnsJson, buildDescribeJson, createLandingDescribe, CUPOLA_BASE, LANDING_SCHEMA_VERSION, type LandingDescribeProvider, type WorkerDescribeInfo, } from "./http/describe-json.js";
26
27
  export { BindResultSchema, ScanFunctionResultSchema } from "./generated/vgi-protocol-schemas.js";
27
28
  export { type AttachOptionSpec, serializeAttachOptionSpec, serializeAttachOptionSpecs } from "./catalog/attach-option.js";
28
29
  export { type ColumnStatistics, serializeColumnStatistics } from "./util/statistics.js";
package/dist/index.js CHANGED
@@ -9929,6 +9929,321 @@ function buildVgiProtocol(config) {
9929
9929
  registerCatalogMethods(protocol, config.catalogInterface, config.catalogName, config.signingKey);
9930
9930
  return protocol;
9931
9931
  }
9932
+ // src/http/describe-json.ts
9933
+ init_arrow();
9934
+ var LANDING_SCHEMA_VERSION = 1;
9935
+ var CUPOLA_BASE = "https://cupola.query-farm.services";
9936
+ var STRING_TAGS = {
9937
+ title: "vgi.title",
9938
+ doc_md: "vgi.doc_md",
9939
+ source_url: "vgi.source_url",
9940
+ license: "vgi.license",
9941
+ author: "vgi.author",
9942
+ copyright: "vgi.copyright",
9943
+ support_contact: "vgi.support_contact",
9944
+ support_policy_url: "vgi.support_policy_url"
9945
+ };
9946
+ var KEYWORDS_TAG = "vgi.keywords";
9947
+ function typeToString(type) {
9948
+ if (isNull(type))
9949
+ return "NULL";
9950
+ if (isUtf8(type))
9951
+ return "VARCHAR";
9952
+ if (isBinary(type))
9953
+ return "BLOB";
9954
+ if (isBool(type))
9955
+ return "BOOLEAN";
9956
+ if (isInt(type)) {
9957
+ const bw = type.bitWidth ?? 32;
9958
+ return bw === 64 ? "BIGINT" : "INTEGER";
9959
+ }
9960
+ if (isFloat(type)) {
9961
+ const precision = type.precision ?? 2;
9962
+ return precision === 1 ? "FLOAT" : "DOUBLE";
9963
+ }
9964
+ const s = type.toString?.();
9965
+ return s ?? String(type.typeId);
9966
+ }
9967
+ function fieldMeta(field3, key) {
9968
+ return field3.metadata.get(key);
9969
+ }
9970
+ function fieldToColumn(field3) {
9971
+ const col = { name: field3.name, type: typeToString(field3.type) };
9972
+ const comment = fieldMeta(field3, "comment") ?? fieldMeta(field3, VGI_DOC_KEY);
9973
+ if (comment)
9974
+ col.comment = comment;
9975
+ return col;
9976
+ }
9977
+ function readSchemaSafe(bytes) {
9978
+ if (!bytes || bytes.length === 0)
9979
+ return null;
9980
+ try {
9981
+ return deserializeSchema(bytes);
9982
+ } catch {
9983
+ return null;
9984
+ }
9985
+ }
9986
+ function functionDisplayType(fn) {
9987
+ const t = String(fn.function_type).toUpperCase();
9988
+ if (t === "SCALAR")
9989
+ return "scalar";
9990
+ if (t === "AGGREGATE")
9991
+ return "aggregate";
9992
+ if (fn.has_finalize)
9993
+ return "table_in_out";
9994
+ return "table";
9995
+ }
9996
+ function functionReturns(fn) {
9997
+ const schema3 = readSchemaSafe(fn.output_schema);
9998
+ if (schema3 === null || schema3.fields.length === 0)
9999
+ return;
10000
+ const t = String(fn.function_type).toUpperCase();
10001
+ if (t === "SCALAR" || t === "AGGREGATE") {
10002
+ return typeToString(schema3.fields[0].type);
10003
+ }
10004
+ const cols = schema3.fields.map((f) => `${f.name} ${typeToString(f.type)}`).join(", ");
10005
+ return `TABLE(${cols})`;
10006
+ }
10007
+ function functionArgs(fn) {
10008
+ const schema3 = readSchemaSafe(fn.arguments);
10009
+ if (schema3 === null)
10010
+ return [];
10011
+ const args = [];
10012
+ for (const field3 of schema3.fields) {
10013
+ if (fieldMeta(field3, VGI_TYPE_KEY) === VGI_TYPE_TABLE)
10014
+ continue;
10015
+ const arg = { name: field3.name, type: typeToString(field3.type) };
10016
+ if (fieldMeta(field3, VGI_ARG_KEY) === VGI_ARG_NAMED)
10017
+ arg.named = true;
10018
+ const doc = fieldMeta(field3, VGI_DOC_KEY);
10019
+ if (doc)
10020
+ arg.desc = doc;
10021
+ const rawDefault = fieldMeta(field3, VGI_DEFAULT_KEY);
10022
+ if (rawDefault !== undefined) {
10023
+ try {
10024
+ arg.default = JSON.stringify(JSON.parse(rawDefault));
10025
+ } catch {
10026
+ arg.default = rawDefault;
10027
+ }
10028
+ }
10029
+ args.push(arg);
10030
+ }
10031
+ return args;
10032
+ }
10033
+ async function schemaContents(iface, aod, name, kind) {
10034
+ try {
10035
+ if (kind === "TABLE") {
10036
+ return await iface.schemaContentsTables(aod, name);
10037
+ }
10038
+ return await iface.schemaContentsViews(aod, name);
10039
+ } catch {
10040
+ return [];
10041
+ }
10042
+ }
10043
+ async function schemaFunctions(iface, aod, name) {
10044
+ const out = [];
10045
+ for (const kind of ["SCALAR_FUNCTION", "TABLE_FUNCTION", "AGGREGATE_FUNCTION"]) {
10046
+ try {
10047
+ out.push(...await iface.schemaContentsFunctions(aod, name, kind));
10048
+ } catch {}
10049
+ }
10050
+ return out;
10051
+ }
10052
+ async function buildSchemas(iface, aod) {
10053
+ let schemaInfos;
10054
+ try {
10055
+ schemaInfos = await iface.schemas(aod);
10056
+ } catch {
10057
+ return { schemas: [], counts: { schemas: 0, tables: 0, views: 0, functions: 0 } };
10058
+ }
10059
+ const schemas = [];
10060
+ const counts = { schemas: 0, tables: 0, views: 0, functions: 0 };
10061
+ for (const si of schemaInfos) {
10062
+ const tablesRaw = await schemaContents(iface, aod, si.name, "TABLE");
10063
+ const viewsRaw = await schemaContents(iface, aod, si.name, "VIEW");
10064
+ const funcsRaw = await schemaFunctions(iface, aod, si.name);
10065
+ const tables = tablesRaw.map((t) => {
10066
+ const schema3 = readSchemaSafe(t.columns);
10067
+ return { name: t.name, cols: schema3 ? schema3.fields.length : 0, comment: t.comment ?? "" };
10068
+ });
10069
+ const views = viewsRaw.map((v) => ({
10070
+ name: v.name,
10071
+ cols: Object.keys(v.column_comments ?? {}).length,
10072
+ comment: v.comment ?? "",
10073
+ def: v.definition
10074
+ }));
10075
+ const functions = funcsRaw.slice().sort((a, b) => String(a.function_type).localeCompare(String(b.function_type)) || a.name.localeCompare(b.name)).map((fn) => {
10076
+ const obj = {
10077
+ name: fn.name,
10078
+ type: functionDisplayType(fn),
10079
+ doc: fn.description ?? "",
10080
+ args: functionArgs(fn)
10081
+ };
10082
+ const ret = functionReturns(fn);
10083
+ if (ret)
10084
+ obj.returns = ret;
10085
+ return obj;
10086
+ });
10087
+ schemas.push({ name: si.name, tables, views, functions });
10088
+ counts.schemas += 1;
10089
+ counts.tables += tables.length;
10090
+ counts.views += views.length;
10091
+ counts.functions += functions.length;
10092
+ }
10093
+ return { schemas, counts };
10094
+ }
10095
+ function catalogTags(result) {
10096
+ const tags = result?.tags;
10097
+ if (!tags)
10098
+ return {};
10099
+ const out = {};
10100
+ for (const [key, tag] of Object.entries(STRING_TAGS)) {
10101
+ const val = tags[tag];
10102
+ if (val)
10103
+ out[key] = val;
10104
+ }
10105
+ const kw = tags[KEYWORDS_TAG];
10106
+ if (kw) {
10107
+ try {
10108
+ const parsed = JSON.parse(kw);
10109
+ if (Array.isArray(parsed))
10110
+ out.keywords = parsed.map((k) => String(k));
10111
+ } catch {}
10112
+ }
10113
+ return out;
10114
+ }
10115
+ function decodeAttachOption(bytes) {
10116
+ try {
10117
+ const batch = deserializeBatch(bytes);
10118
+ if (batch.numRows === 0)
10119
+ return null;
10120
+ const name = String(batch.getChild("name")?.get(0) ?? "");
10121
+ const description = String(batch.getChild("description")?.get(0) ?? "");
10122
+ const typeBytes = batch.getChild("type")?.get(0);
10123
+ let type = "";
10124
+ const typeSchema = readSchemaSafe(typeBytes ?? null);
10125
+ if (typeSchema && typeSchema.fields.length > 0)
10126
+ type = typeToString(typeSchema.fields[0].type);
10127
+ let defaultStr = "";
10128
+ const defBytes = batch.getChild("default_value")?.get(0);
10129
+ if (defBytes) {
10130
+ try {
10131
+ const defBatch = deserializeBatch(defBytes);
10132
+ if (defBatch.numRows > 0) {
10133
+ const v = defBatch.getChild("value")?.get(0);
10134
+ defaultStr = v == null ? "" : String(v);
10135
+ }
10136
+ } catch {}
10137
+ }
10138
+ return { name, type, default: defaultStr, description };
10139
+ } catch {
10140
+ return null;
10141
+ }
10142
+ }
10143
+ async function attachCatalog(iface, name) {
10144
+ try {
10145
+ return await iface.attach(name, {}, null, null);
10146
+ } catch {
10147
+ return null;
10148
+ }
10149
+ }
10150
+ async function buildCatalog(iface, name, info) {
10151
+ const result = await attachCatalog(iface, name);
10152
+ const attachOptions = (info?.attach_option_specs ?? []).map((b) => decodeAttachOption(b)).filter((o) => o !== null);
10153
+ const dataVersions = (info?.releases ?? []).map((r) => r.summary ? { spec: r.version, label: r.summary } : { spec: r.version });
10154
+ let impl = info?.implementation_version ?? null;
10155
+ let dvs = info?.data_version_spec ?? null;
10156
+ if (result) {
10157
+ impl = result.resolved_implementation_version ?? impl;
10158
+ dvs = result.resolved_data_version ?? dvs;
10159
+ }
10160
+ let schemas = [];
10161
+ let counts = { schemas: 0, tables: 0, views: 0, functions: 0 };
10162
+ if (result) {
10163
+ const built = await buildSchemas(iface, result.attach_opaque_data);
10164
+ schemas = built.schemas;
10165
+ counts = built.counts;
10166
+ }
10167
+ return {
10168
+ name,
10169
+ implementation_version: impl,
10170
+ data_version_spec: dvs,
10171
+ data_versions: dataVersions,
10172
+ attach_options: attachOptions,
10173
+ tags: catalogTags(result),
10174
+ counts,
10175
+ schemas
10176
+ };
10177
+ }
10178
+ async function catalogInfoByName(iface) {
10179
+ const map3 = new Map;
10180
+ if (typeof iface.catalogsInfo === "function") {
10181
+ try {
10182
+ const infos = await iface.catalogsInfo();
10183
+ for (const info of infos)
10184
+ map3.set(info.name, info);
10185
+ } catch {}
10186
+ }
10187
+ return map3;
10188
+ }
10189
+ async function buildDescribeJson(iface, worker, opts = {}) {
10190
+ const infos = await catalogInfoByName(iface);
10191
+ let names;
10192
+ try {
10193
+ names = iface.catalogs();
10194
+ } catch {
10195
+ names = [];
10196
+ }
10197
+ const catalogs = [];
10198
+ for (const name of names) {
10199
+ catalogs.push(await buildCatalog(iface, name, infos.get(name)));
10200
+ }
10201
+ return {
10202
+ landing_schema_version: LANDING_SCHEMA_VERSION,
10203
+ worker: {
10204
+ name: worker.name,
10205
+ doc: worker.doc ?? "",
10206
+ version: worker.version ?? "unknown",
10207
+ lang: "typescript"
10208
+ },
10209
+ server_id: opts.serverId ?? "",
10210
+ oauth: opts.oauth ?? false,
10211
+ cupola_base: opts.cupolaBase ?? CUPOLA_BASE,
10212
+ catalogs
10213
+ };
10214
+ }
10215
+ async function buildColumnsJson(iface, catalog, schema3, table) {
10216
+ const result = await attachCatalog(iface, catalog);
10217
+ if (!result)
10218
+ return null;
10219
+ const aod = result.attach_opaque_data;
10220
+ const tables = await schemaContents(iface, aod, schema3, "TABLE");
10221
+ for (const t of tables) {
10222
+ if (t.name === table) {
10223
+ const arrow = readSchemaSafe(t.columns);
10224
+ return { columns: (arrow?.fields ?? []).map((f) => fieldToColumn(f)) };
10225
+ }
10226
+ }
10227
+ const views = await schemaContents(iface, aod, schema3, "VIEW");
10228
+ for (const v of views) {
10229
+ if (v.name === table) {
10230
+ return {
10231
+ columns: Object.entries(v.column_comments ?? {}).map(([name, comment]) => ({
10232
+ name,
10233
+ type: "",
10234
+ comment
10235
+ }))
10236
+ };
10237
+ }
10238
+ }
10239
+ return null;
10240
+ }
10241
+ function createLandingDescribe(iface, worker, opts = {}) {
10242
+ return {
10243
+ describe: (ctx) => buildDescribeJson(iface, worker, { ...ctx, cupolaBase: opts.cupolaBase }),
10244
+ columns: (catalog, schema3, table) => buildColumnsJson(iface, catalog, schema3, table)
10245
+ };
10246
+ }
9932
10247
  // src/catalog/attach-option.ts
9933
10248
  init_arrow();
9934
10249
  var SPEC_SCHEMA = schema2([
@@ -10989,6 +11304,7 @@ export {
10989
11304
  dateMillisecond2 as dateMillisecond,
10990
11305
  dateDay2 as dateDay,
10991
11306
  date2 as date,
11307
+ createLandingDescribe,
10992
11308
  constraintSpecFields,
10993
11309
  columnFromArray,
10994
11310
  codecFor,
@@ -10996,6 +11312,8 @@ export {
10996
11312
  buildVgiProtocol,
10997
11313
  buildScanBranchesResult,
10998
11314
  buildJoinKeysLookup,
11315
+ buildDescribeJson,
11316
+ buildColumnsJson,
10999
11317
  bool3 as bool,
11000
11318
  binary2 as binary,
11001
11319
  batchToSecretDict,
@@ -11041,6 +11359,7 @@ export {
11041
11359
  OrderDependence,
11042
11360
  NullHandling,
11043
11361
  NoCatalogError,
11362
+ LANDING_SCHEMA_VERSION,
11044
11363
  IntervalUnit2 as IntervalUnit,
11045
11364
  GROUP_COLUMN_NAME,
11046
11365
  FunctionType,
@@ -11061,6 +11380,7 @@ export {
11061
11380
  CatalogInterface,
11062
11381
  CatalogFunctionType,
11063
11382
  CatalogAlreadyExistsError,
11383
+ CUPOLA_BASE,
11064
11384
  BoundStorage,
11065
11385
  BindResultSchema,
11066
11386
  AuthContext,
@@ -11068,4 +11388,4 @@ export {
11068
11388
  ArgumentValidationError
11069
11389
  };
11070
11390
 
11071
- //# debugId=606DBE277F7C8D1D64756E2164756E21
11391
+ //# debugId=FB551171F4FDA15864756E2164756E21