@querypanel/node-sdk 1.3.5 → 1.3.7
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 +24 -0
- package/dist/index.cjs +525 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +183 -8
- package/dist/index.d.ts +183 -8
- package/dist/index.js +524 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
QueryErrorCode: () => QueryErrorCode,
|
|
39
39
|
QueryPanelSdkAPI: () => QueryPanelSdkAPI,
|
|
40
40
|
QueryPipelineError: () => QueryPipelineError,
|
|
41
|
+
SchemaTooLargeError: () => SchemaTooLargeError,
|
|
41
42
|
anonymizeResults: () => anonymizeResults,
|
|
42
43
|
mapGeneratedParams: () => mapGeneratedParams,
|
|
43
44
|
normalizeSqlParamsInput: () => normalizeSqlParamsInput,
|
|
@@ -45,6 +46,16 @@ __export(index_exports, {
|
|
|
45
46
|
});
|
|
46
47
|
module.exports = __toCommonJS(index_exports);
|
|
47
48
|
|
|
49
|
+
// src/adapters/types.ts
|
|
50
|
+
var DEFAULT_CATALOG_LIMIT = 500;
|
|
51
|
+
var MAX_CATALOG_LIMIT = 5e3;
|
|
52
|
+
function resolveCatalogLimit(limit) {
|
|
53
|
+
if (limit === void 0 || !Number.isFinite(limit)) {
|
|
54
|
+
return DEFAULT_CATALOG_LIMIT;
|
|
55
|
+
}
|
|
56
|
+
return Math.min(Math.max(Math.floor(limit), 1), MAX_CATALOG_LIMIT);
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
// src/adapters/bigquery.ts
|
|
49
60
|
var BigQueryAdapter = class {
|
|
50
61
|
constructor(clientFn, options) {
|
|
@@ -153,6 +164,53 @@ var BigQueryAdapter = class {
|
|
|
153
164
|
introspectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
154
165
|
};
|
|
155
166
|
}
|
|
167
|
+
/** The adapter is bound to one dataset, so this returns a single row. */
|
|
168
|
+
async listSchemas() {
|
|
169
|
+
const result = await this.clientFn({
|
|
170
|
+
query: `SELECT COUNT(*) AS table_count
|
|
171
|
+
FROM ${this.informationSchema("TABLES")}`,
|
|
172
|
+
dryRun: false
|
|
173
|
+
});
|
|
174
|
+
return [
|
|
175
|
+
{
|
|
176
|
+
schema: this.defaultDataset,
|
|
177
|
+
tableCount: Number(result.rows[0]?.table_count ?? 0)
|
|
178
|
+
}
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* List tables in the bound dataset. Reads the full catalog (not restricted
|
|
183
|
+
* by `allowedTables`). Default limit 500, max 5000.
|
|
184
|
+
*/
|
|
185
|
+
async listTables(options = {}) {
|
|
186
|
+
if (options.schema && options.schema !== this.defaultDataset) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const search = options.search?.trim();
|
|
190
|
+
const params = {};
|
|
191
|
+
let searchClause = "";
|
|
192
|
+
if (search) {
|
|
193
|
+
params.search = `%${search.toLowerCase().replace(/[\\%_]/g, (ch) => `\\${ch}`)}%`;
|
|
194
|
+
searchClause = "\n WHERE LOWER(table_name) LIKE @search";
|
|
195
|
+
}
|
|
196
|
+
const limit = resolveCatalogLimit(options.limit);
|
|
197
|
+
const result = await this.clientFn({
|
|
198
|
+
query: `SELECT table_schema, table_name, table_type
|
|
199
|
+
FROM ${this.informationSchema("TABLES")}${searchClause}
|
|
200
|
+
ORDER BY table_name
|
|
201
|
+
LIMIT ${limit}`,
|
|
202
|
+
...search ? { params } : {},
|
|
203
|
+
dryRun: false
|
|
204
|
+
});
|
|
205
|
+
return result.rows.map((row) => ({
|
|
206
|
+
schema: row.table_schema,
|
|
207
|
+
name: row.table_name,
|
|
208
|
+
type: asTableType(row.table_type)
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
informationSchema(view) {
|
|
212
|
+
return `\`${this.datasetProjectId}.${this.defaultDataset}.INFORMATION_SCHEMA.${view}\``;
|
|
213
|
+
}
|
|
156
214
|
validateQueryTables(sql) {
|
|
157
215
|
if (!this.allowedTables || this.allowedTables.length === 0) {
|
|
158
216
|
return;
|
|
@@ -183,8 +241,10 @@ function normalizeTableFilter(tables, defaultDataset) {
|
|
|
183
241
|
for (const raw of tables) {
|
|
184
242
|
if (!raw?.trim()) continue;
|
|
185
243
|
const trimmed = raw.trim();
|
|
244
|
+
if (trimmed === "*" || trimmed === `${defaultDataset}.*`) return [];
|
|
186
245
|
const parts = trimmed.split(".");
|
|
187
246
|
const table = parts.pop() ?? "";
|
|
247
|
+
if (table === "*") continue;
|
|
188
248
|
const dataset = parts.pop() ?? defaultDataset;
|
|
189
249
|
const key = tableKey(dataset, table);
|
|
190
250
|
if (seen.has(key)) continue;
|
|
@@ -295,7 +355,10 @@ var ClickHouseAdapter = class {
|
|
|
295
355
|
this.defaultFormat = options.defaultFormat ?? "JSONEachRow";
|
|
296
356
|
this.kind = options.kind ?? "clickhouse";
|
|
297
357
|
if (options.allowedTables) {
|
|
298
|
-
this.allowedTables = normalizeTableFilter2(
|
|
358
|
+
this.allowedTables = normalizeTableFilter2(
|
|
359
|
+
options.allowedTables,
|
|
360
|
+
this.databaseName
|
|
361
|
+
);
|
|
299
362
|
}
|
|
300
363
|
}
|
|
301
364
|
clientFn;
|
|
@@ -334,7 +397,7 @@ var ClickHouseAdapter = class {
|
|
|
334
397
|
* No indexes, constraints, or statistics
|
|
335
398
|
*/
|
|
336
399
|
async introspect(options) {
|
|
337
|
-
const tablesToIntrospect = options?.tables ? normalizeTableFilter2(options.tables) : this.allowedTables;
|
|
400
|
+
const tablesToIntrospect = options?.tables ? normalizeTableFilter2(options.tables, this.databaseName) : this.allowedTables;
|
|
338
401
|
const allowTables = tablesToIntrospect ?? [];
|
|
339
402
|
const hasFilter = allowTables.length > 0;
|
|
340
403
|
const queryParams = {
|
|
@@ -392,6 +455,53 @@ var ClickHouseAdapter = class {
|
|
|
392
455
|
introspectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
393
456
|
};
|
|
394
457
|
}
|
|
458
|
+
/** The adapter is bound to one database, so this returns a single row. */
|
|
459
|
+
async listSchemas() {
|
|
460
|
+
const rows = await this.query(
|
|
461
|
+
`SELECT count() AS table_count
|
|
462
|
+
FROM system.tables
|
|
463
|
+
WHERE database = {db:String}`,
|
|
464
|
+
{ params: { db: this.databaseName } }
|
|
465
|
+
);
|
|
466
|
+
return [
|
|
467
|
+
{
|
|
468
|
+
schema: this.databaseName,
|
|
469
|
+
tableCount: toNumber(rows[0]?.table_count) ?? 0
|
|
470
|
+
}
|
|
471
|
+
];
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* List tables in the bound database. Reads the full catalog (not restricted
|
|
475
|
+
* by `allowedTables`). Default limit 500, max 5000.
|
|
476
|
+
*/
|
|
477
|
+
async listTables(options = {}) {
|
|
478
|
+
if (options.schema && options.schema !== this.databaseName) {
|
|
479
|
+
return [];
|
|
480
|
+
}
|
|
481
|
+
const params = {
|
|
482
|
+
db: this.databaseName,
|
|
483
|
+
limit: resolveCatalogLimit(options.limit)
|
|
484
|
+
};
|
|
485
|
+
const search = options.search?.trim();
|
|
486
|
+
let searchClause = "";
|
|
487
|
+
if (search) {
|
|
488
|
+
params.search = search;
|
|
489
|
+
searchClause = " AND positionCaseInsensitive(name, {search:String}) > 0";
|
|
490
|
+
}
|
|
491
|
+
const rows = await this.query(
|
|
492
|
+
`SELECT name, engine
|
|
493
|
+
FROM system.tables
|
|
494
|
+
WHERE database = {db:String}${searchClause}
|
|
495
|
+
ORDER BY name
|
|
496
|
+
LIMIT {limit:UInt32}`,
|
|
497
|
+
{ params }
|
|
498
|
+
);
|
|
499
|
+
return rows.map((row) => ({
|
|
500
|
+
schema: this.databaseName,
|
|
501
|
+
name: row.name,
|
|
502
|
+
type: asCatalogTableType(row.engine)
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
395
505
|
validateQueryTables(sql) {
|
|
396
506
|
if (!this.allowedTables || this.allowedTables.length === 0) {
|
|
397
507
|
return;
|
|
@@ -457,7 +567,7 @@ function normalizePayload(payload) {
|
|
|
457
567
|
}
|
|
458
568
|
return [];
|
|
459
569
|
}
|
|
460
|
-
function normalizeTableFilter2(tables) {
|
|
570
|
+
function normalizeTableFilter2(tables, databaseName) {
|
|
461
571
|
if (!tables?.length) return [];
|
|
462
572
|
const seen = /* @__PURE__ */ new Set();
|
|
463
573
|
const normalized = [];
|
|
@@ -465,8 +575,10 @@ function normalizeTableFilter2(tables) {
|
|
|
465
575
|
if (!table) continue;
|
|
466
576
|
const trimmed = table.trim();
|
|
467
577
|
if (!trimmed) continue;
|
|
578
|
+
if (trimmed === "*" || trimmed === `${databaseName}.*`) return [];
|
|
468
579
|
const parts = trimmed.split(".");
|
|
469
580
|
const tableName = parts[parts.length - 1];
|
|
581
|
+
if (tableName === "*") continue;
|
|
470
582
|
if (!tableName || seen.has(tableName)) continue;
|
|
471
583
|
seen.add(tableName);
|
|
472
584
|
normalized.push(tableName);
|
|
@@ -499,6 +611,12 @@ function asTableType2(engine) {
|
|
|
499
611
|
}
|
|
500
612
|
return "table";
|
|
501
613
|
}
|
|
614
|
+
function asCatalogTableType(engine) {
|
|
615
|
+
const normalized = typeof engine === "string" ? engine.toLowerCase() : "";
|
|
616
|
+
if (normalized === "materializedview") return "materialized_view";
|
|
617
|
+
if (normalized.includes("view")) return "view";
|
|
618
|
+
return "table";
|
|
619
|
+
}
|
|
502
620
|
function sanitize(value) {
|
|
503
621
|
if (value === null || value === void 0) return void 0;
|
|
504
622
|
const trimmed = String(value).trim();
|
|
@@ -512,6 +630,7 @@ function toNumber(value) {
|
|
|
512
630
|
}
|
|
513
631
|
|
|
514
632
|
// src/adapters/postgres.ts
|
|
633
|
+
var WILDCARD = "*";
|
|
515
634
|
function maxPostgresPlaceholderIndex(sql) {
|
|
516
635
|
let max = 0;
|
|
517
636
|
for (const match of sql.matchAll(/\$(\d+)\b/g)) {
|
|
@@ -583,6 +702,9 @@ var PostgresAdapter = class {
|
|
|
583
702
|
const allowedSet = new Set(
|
|
584
703
|
this.allowedTables.map((t) => tableKey2(t.schema, t.table))
|
|
585
704
|
);
|
|
705
|
+
const allowedSchemas = new Set(
|
|
706
|
+
this.allowedTables.filter((t) => t.table === WILDCARD).map((t) => t.schema)
|
|
707
|
+
);
|
|
586
708
|
const neutralizedSql = sql.replace(/EXTRACT\s*\([^)]*FROM\s+[^)]+\)/gi, "EXTRACT(/*neutralized*/)").replace(/SUBSTRING\s*\([^)]*FROM\s+[^)]+\)/gi, "SUBSTRING(/*neutralized*/)").replace(/TRIM\s*\([^)]*FROM\s+[^)]+\)/gi, "TRIM(/*neutralized*/)").replace(/POSITION\s*\([^)]*FROM\s+[^)]+\)/gi, "POSITION(/*neutralized*/)");
|
|
587
709
|
const tablePattern = /(?:FROM|JOIN)\s+(?:ONLY\s+)?(?:([a-zA-Z_][a-zA-Z0-9_]*)\.)?(["']?[a-zA-Z_][a-zA-Z0-9_]*["']?)/gi;
|
|
588
710
|
const matches = neutralizedSql.matchAll(tablePattern);
|
|
@@ -591,7 +713,7 @@ var PostgresAdapter = class {
|
|
|
591
713
|
const table = match[2]?.replace(/['"]/g, "");
|
|
592
714
|
if (table) {
|
|
593
715
|
const key = tableKey2(schema, table);
|
|
594
|
-
if (!allowedSet.has(key)) {
|
|
716
|
+
if (!allowedSet.has(key) && !allowedSchemas.has(schema)) {
|
|
595
717
|
throw new Error(
|
|
596
718
|
`Query references table "${schema}.${table}" which is not in the allowed tables list`
|
|
597
719
|
);
|
|
@@ -643,6 +765,33 @@ var PostgresAdapter = class {
|
|
|
643
765
|
getDialect() {
|
|
644
766
|
return this.dialect;
|
|
645
767
|
}
|
|
768
|
+
/**
|
|
769
|
+
* List schemas with their table/view counts. Reads the full catalog (not
|
|
770
|
+
* restricted by `allowedTables`) so it can back an allowed-tables picker.
|
|
771
|
+
*/
|
|
772
|
+
async listSchemas() {
|
|
773
|
+
const result = await this.clientFn(
|
|
774
|
+
this.dialect === "redshift" ? buildRedshiftListSchemasQuery() : buildListSchemasQuery()
|
|
775
|
+
);
|
|
776
|
+
return result.rows.map((row) => ({
|
|
777
|
+
schema: String(row.schema),
|
|
778
|
+
tableCount: Number(row.table_count ?? 0)
|
|
779
|
+
}));
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* List tables/views, optionally filtered by schema and a case-insensitive
|
|
783
|
+
* substring of the table name. Reads the full catalog (not restricted by
|
|
784
|
+
* `allowedTables`). Default limit 500, max 5000.
|
|
785
|
+
*/
|
|
786
|
+
async listTables(options = {}) {
|
|
787
|
+
const { sql, params } = buildListTablesQuery(this.dialect, options);
|
|
788
|
+
const result = await this.clientFn(sql, params);
|
|
789
|
+
return result.rows.map((row) => ({
|
|
790
|
+
schema: String(row.schema),
|
|
791
|
+
name: String(row.name),
|
|
792
|
+
type: asTableType3(String(row.table_type ?? ""))
|
|
793
|
+
}));
|
|
794
|
+
}
|
|
646
795
|
/**
|
|
647
796
|
* Simplified introspection: only collect table/column metadata for IngestRequest
|
|
648
797
|
* No indexes, constraints, or statistics
|
|
@@ -716,7 +865,7 @@ function normalizeTableFilter3(tables, defaultSchema) {
|
|
|
716
865
|
const parts = trimmed.split(".");
|
|
717
866
|
const table = parts.pop() ?? "";
|
|
718
867
|
const schema = parts.pop() ?? defaultSchema;
|
|
719
|
-
if (!isSafeIdentifier(schema) || !isSafeIdentifier(table)) {
|
|
868
|
+
if (!isSafeIdentifier(schema) || table !== WILDCARD && !isSafeIdentifier(table)) {
|
|
720
869
|
continue;
|
|
721
870
|
}
|
|
722
871
|
const key = tableKey2(schema, table);
|
|
@@ -818,11 +967,83 @@ function buildRedshiftColumnsQuery(tables) {
|
|
|
818
967
|
}
|
|
819
968
|
function buildFilterClause(tables, schemaExpr, tableExpr) {
|
|
820
969
|
if (!tables.length) return "";
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
970
|
+
const wildcardSchemas = new Set(
|
|
971
|
+
tables.filter((t) => t.table === WILDCARD).map((t) => t.schema)
|
|
972
|
+
);
|
|
973
|
+
const clauses = [];
|
|
974
|
+
for (const schema of wildcardSchemas) {
|
|
975
|
+
clauses.push(`(${schemaExpr} = '${schema}')`);
|
|
976
|
+
}
|
|
977
|
+
for (const { schema, table } of tables) {
|
|
978
|
+
if (table === WILDCARD || wildcardSchemas.has(schema)) continue;
|
|
979
|
+
clauses.push(`(${schemaExpr} = '${schema}' AND ${tableExpr} = '${table}')`);
|
|
980
|
+
}
|
|
824
981
|
return `AND (${clauses.join(" OR ")})`;
|
|
825
982
|
}
|
|
983
|
+
function buildListSchemasQuery() {
|
|
984
|
+
return `SELECT n.nspname AS schema, COUNT(*) AS table_count
|
|
985
|
+
FROM pg_class c
|
|
986
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
987
|
+
WHERE c.relkind IN ('r', 'v', 'm')
|
|
988
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
989
|
+
AND n.nspname NOT LIKE 'pg_temp%'
|
|
990
|
+
AND n.nspname NOT LIKE 'pg_toast_temp%'
|
|
991
|
+
GROUP BY n.nspname
|
|
992
|
+
ORDER BY n.nspname;`;
|
|
993
|
+
}
|
|
994
|
+
function buildRedshiftListSchemasQuery() {
|
|
995
|
+
return `SELECT table_schema AS schema, COUNT(*) AS table_count
|
|
996
|
+
FROM svv_tables
|
|
997
|
+
WHERE table_catalog = current_database()
|
|
998
|
+
AND table_schema NOT IN ${REDSHIFT_EXCLUDED_SCHEMAS}
|
|
999
|
+
AND table_schema NOT LIKE 'pg_temp%'
|
|
1000
|
+
GROUP BY table_schema
|
|
1001
|
+
ORDER BY table_schema;`;
|
|
1002
|
+
}
|
|
1003
|
+
function toLikeSubstringPattern(search) {
|
|
1004
|
+
return `%${search.replace(/[\\%_]/g, (ch) => `\\${ch}`)}%`;
|
|
1005
|
+
}
|
|
1006
|
+
function buildListTablesQuery(dialect, options) {
|
|
1007
|
+
const isRedshift = dialect === "redshift";
|
|
1008
|
+
const schemaExpr = isRedshift ? "table_schema" : "n.nspname";
|
|
1009
|
+
const tableExpr = isRedshift ? "table_name" : "c.relname";
|
|
1010
|
+
const params = [];
|
|
1011
|
+
const filters = [];
|
|
1012
|
+
if (options.schema) {
|
|
1013
|
+
params.push(options.schema);
|
|
1014
|
+
filters.push(`AND ${schemaExpr} = $${params.length}`);
|
|
1015
|
+
}
|
|
1016
|
+
const search = options.search?.trim();
|
|
1017
|
+
if (search) {
|
|
1018
|
+
params.push(toLikeSubstringPattern(search));
|
|
1019
|
+
filters.push(`AND ${tableExpr} ILIKE $${params.length}`);
|
|
1020
|
+
}
|
|
1021
|
+
const limit = resolveCatalogLimit(options.limit);
|
|
1022
|
+
const filterSql = filters.join("\n ");
|
|
1023
|
+
const sql = isRedshift ? `SELECT table_schema AS schema, table_name AS name, table_type
|
|
1024
|
+
FROM svv_tables
|
|
1025
|
+
WHERE table_catalog = current_database()
|
|
1026
|
+
AND table_schema NOT IN ${REDSHIFT_EXCLUDED_SCHEMAS}
|
|
1027
|
+
AND table_schema NOT LIKE 'pg_temp%'
|
|
1028
|
+
${filterSql}
|
|
1029
|
+
ORDER BY table_schema, table_name
|
|
1030
|
+
LIMIT ${limit};` : `SELECT n.nspname AS schema, c.relname AS name,
|
|
1031
|
+
CASE c.relkind
|
|
1032
|
+
WHEN 'v' THEN 'view'
|
|
1033
|
+
WHEN 'm' THEN 'materialized_view'
|
|
1034
|
+
ELSE 'table'
|
|
1035
|
+
END AS table_type
|
|
1036
|
+
FROM pg_class c
|
|
1037
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
1038
|
+
WHERE c.relkind IN ('r', 'v', 'm')
|
|
1039
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
1040
|
+
AND n.nspname NOT LIKE 'pg_temp%'
|
|
1041
|
+
AND n.nspname NOT LIKE 'pg_toast_temp%'
|
|
1042
|
+
${filterSql}
|
|
1043
|
+
ORDER BY n.nspname, c.relname
|
|
1044
|
+
LIMIT ${limit};`;
|
|
1045
|
+
return { sql, params };
|
|
1046
|
+
}
|
|
826
1047
|
function tableKey2(schema, table) {
|
|
827
1048
|
return `${schema}.${table}`;
|
|
828
1049
|
}
|
|
@@ -957,6 +1178,20 @@ var QueryPipelineError = class extends Error {
|
|
|
957
1178
|
return isAiProviderErrorCode(this.code);
|
|
958
1179
|
}
|
|
959
1180
|
};
|
|
1181
|
+
var SchemaTooLargeError = class extends Error {
|
|
1182
|
+
constructor(tableCount, columnCount, maxColumns) {
|
|
1183
|
+
super(
|
|
1184
|
+
`Schema too large to sync: ${columnCount} columns across ${tableCount} tables exceeds the limit of ${maxColumns} columns. Restrict the tables to sync (e.g. allowedTables with "schema.*" or specific tables).`
|
|
1185
|
+
);
|
|
1186
|
+
this.tableCount = tableCount;
|
|
1187
|
+
this.columnCount = columnCount;
|
|
1188
|
+
this.maxColumns = maxColumns;
|
|
1189
|
+
this.name = "SchemaTooLargeError";
|
|
1190
|
+
}
|
|
1191
|
+
tableCount;
|
|
1192
|
+
columnCount;
|
|
1193
|
+
maxColumns;
|
|
1194
|
+
};
|
|
960
1195
|
|
|
961
1196
|
// src/core/client.ts
|
|
962
1197
|
var ApiClient = class {
|
|
@@ -1096,7 +1331,7 @@ var ApiClient = class {
|
|
|
1096
1331
|
json = void 0;
|
|
1097
1332
|
}
|
|
1098
1333
|
if (!response.ok) {
|
|
1099
|
-
const message = json
|
|
1334
|
+
const message = errorMessageFromBody(json, response);
|
|
1100
1335
|
if (isQueryErrorCode(json?.code)) {
|
|
1101
1336
|
throw new QueryPipelineError(
|
|
1102
1337
|
message,
|
|
@@ -1218,6 +1453,17 @@ var ApiClient = class {
|
|
|
1218
1453
|
return `${data}.${encodedSignature}`;
|
|
1219
1454
|
}
|
|
1220
1455
|
};
|
|
1456
|
+
function errorMessageFromBody(json, response) {
|
|
1457
|
+
const error = json?.error;
|
|
1458
|
+
if (typeof error === "string" && error) return error;
|
|
1459
|
+
if (error && typeof error === "object") {
|
|
1460
|
+
const nested = error.message ?? error.code;
|
|
1461
|
+
if (typeof nested === "string" && nested) {
|
|
1462
|
+
return `${nested} (HTTP ${response.status})`;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return response.statusText || `Request failed (HTTP ${response.status})`;
|
|
1466
|
+
}
|
|
1221
1467
|
|
|
1222
1468
|
// src/core/callback-client.ts
|
|
1223
1469
|
var CallbackApiClient = class {
|
|
@@ -2004,6 +2250,191 @@ async function syncSchema(client, queryEngine, databaseName, options, signal) {
|
|
|
2004
2250
|
);
|
|
2005
2251
|
return response;
|
|
2006
2252
|
}
|
|
2253
|
+
var DEFAULT_MAX_BATCH_DOCS = 1e3;
|
|
2254
|
+
var DEFAULT_MAX_BATCH_BYTES = 2e6;
|
|
2255
|
+
var utf8 = new TextEncoder();
|
|
2256
|
+
function jsonBytes(value) {
|
|
2257
|
+
return utf8.encode(JSON.stringify(value)).length;
|
|
2258
|
+
}
|
|
2259
|
+
var BATCH_ENVELOPE_BYTES = jsonBytes({ tables: [] });
|
|
2260
|
+
function splitIngestTablesIntoBatches(tables, maxBatchDocs = DEFAULT_MAX_BATCH_DOCS, maxBatchBytes = DEFAULT_MAX_BATCH_BYTES) {
|
|
2261
|
+
const batches = [];
|
|
2262
|
+
let current = [];
|
|
2263
|
+
let currentDocs = 0;
|
|
2264
|
+
let currentBytes = BATCH_ENVELOPE_BYTES;
|
|
2265
|
+
for (const table of tables) {
|
|
2266
|
+
const docs = 1 + table.columns.length;
|
|
2267
|
+
const bytes = jsonBytes(table);
|
|
2268
|
+
if (current.length > 0 && (currentDocs + docs > maxBatchDocs || currentBytes + 1 + bytes > maxBatchBytes)) {
|
|
2269
|
+
batches.push(current);
|
|
2270
|
+
current = [];
|
|
2271
|
+
currentDocs = 0;
|
|
2272
|
+
currentBytes = BATCH_ENVELOPE_BYTES;
|
|
2273
|
+
}
|
|
2274
|
+
currentBytes += bytes + (current.length > 0 ? 1 : 0);
|
|
2275
|
+
current.push(table);
|
|
2276
|
+
currentDocs += docs;
|
|
2277
|
+
}
|
|
2278
|
+
if (current.length > 0 || batches.length === 0) {
|
|
2279
|
+
batches.push(current);
|
|
2280
|
+
}
|
|
2281
|
+
return batches;
|
|
2282
|
+
}
|
|
2283
|
+
function sessionPath(sessionId, suffix) {
|
|
2284
|
+
return `/ingest/sessions/${encodeURIComponent(sessionId)}${suffix}`;
|
|
2285
|
+
}
|
|
2286
|
+
async function stageSchemaIngest(client, queryEngine, databaseName, options, signal) {
|
|
2287
|
+
const tenantId = resolveTenantId4(client, options.tenantId);
|
|
2288
|
+
const adapter = queryEngine.getDatabase(databaseName);
|
|
2289
|
+
const metadata = queryEngine.getDatabaseMetadata(databaseName);
|
|
2290
|
+
const emit = options.onProgress ?? (() => {
|
|
2291
|
+
});
|
|
2292
|
+
emit({ phase: "introspecting" });
|
|
2293
|
+
const introspection = await adapter.introspect(
|
|
2294
|
+
options.tables ? { tables: options.tables } : void 0
|
|
2295
|
+
);
|
|
2296
|
+
const { tables, ...sessionFields } = buildSchemaRequest(
|
|
2297
|
+
databaseName,
|
|
2298
|
+
adapter,
|
|
2299
|
+
introspection,
|
|
2300
|
+
metadata
|
|
2301
|
+
);
|
|
2302
|
+
const tableCount = tables.length;
|
|
2303
|
+
const columnCount = tables.reduce((sum, t) => sum + t.columns.length, 0);
|
|
2304
|
+
emit({ phase: "introspected", tableCount, columnCount });
|
|
2305
|
+
if (options.maxColumns !== void 0 && columnCount > options.maxColumns) {
|
|
2306
|
+
throw new SchemaTooLargeError(tableCount, columnCount, options.maxColumns);
|
|
2307
|
+
}
|
|
2308
|
+
const batches = splitIngestTablesIntoBatches(
|
|
2309
|
+
tables,
|
|
2310
|
+
options.maxBatchDocs ?? DEFAULT_MAX_BATCH_DOCS,
|
|
2311
|
+
options.maxBatchBytes ?? DEFAULT_MAX_BATCH_BYTES
|
|
2312
|
+
);
|
|
2313
|
+
const totalBatches = batches.length;
|
|
2314
|
+
const created = await client.post(
|
|
2315
|
+
"/ingest/sessions",
|
|
2316
|
+
{
|
|
2317
|
+
...sessionFields,
|
|
2318
|
+
...options.forceReindex ? { force_reindex: true } : {},
|
|
2319
|
+
total_batches: totalBatches
|
|
2320
|
+
},
|
|
2321
|
+
tenantId,
|
|
2322
|
+
options.userId,
|
|
2323
|
+
options.scopes,
|
|
2324
|
+
signal,
|
|
2325
|
+
import_node_crypto2.default.randomUUID()
|
|
2326
|
+
);
|
|
2327
|
+
const sessionId = created.session_id;
|
|
2328
|
+
for (let index = 0; index < totalBatches; index += 1) {
|
|
2329
|
+
await client.put(
|
|
2330
|
+
sessionPath(sessionId, `/batches/${index}`),
|
|
2331
|
+
{ tables: batches[index] },
|
|
2332
|
+
tenantId,
|
|
2333
|
+
options.userId,
|
|
2334
|
+
options.scopes,
|
|
2335
|
+
signal,
|
|
2336
|
+
sessionId
|
|
2337
|
+
);
|
|
2338
|
+
emit({ phase: "uploading", done: index + 1, total: totalBatches });
|
|
2339
|
+
}
|
|
2340
|
+
emit({ phase: "committing" });
|
|
2341
|
+
const committed = await client.post(
|
|
2342
|
+
sessionPath(sessionId, "/commit"),
|
|
2343
|
+
{},
|
|
2344
|
+
tenantId,
|
|
2345
|
+
options.userId,
|
|
2346
|
+
options.scopes,
|
|
2347
|
+
signal,
|
|
2348
|
+
sessionId
|
|
2349
|
+
);
|
|
2350
|
+
if (committed.status === "skipped") {
|
|
2351
|
+
const { status: _status, ...skippedResult } = committed;
|
|
2352
|
+
return {
|
|
2353
|
+
sessionId,
|
|
2354
|
+
status: "skipped",
|
|
2355
|
+
totalBatches,
|
|
2356
|
+
tableCount,
|
|
2357
|
+
columnCount,
|
|
2358
|
+
skippedResult,
|
|
2359
|
+
driftDetected: skippedResult.drift_detected
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
return {
|
|
2363
|
+
sessionId,
|
|
2364
|
+
status: "embedding",
|
|
2365
|
+
totalBatches: committed.total_batches ?? totalBatches,
|
|
2366
|
+
tableCount,
|
|
2367
|
+
columnCount,
|
|
2368
|
+
driftDetected: committed.drift_detected
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
async function embedSchemaIngestBatch(client, sessionId, index, options, signal) {
|
|
2372
|
+
const tenantId = resolveTenantId4(client, options.tenantId);
|
|
2373
|
+
return await client.post(
|
|
2374
|
+
sessionPath(sessionId, `/batches/${index}/embed`),
|
|
2375
|
+
{},
|
|
2376
|
+
tenantId,
|
|
2377
|
+
options.userId,
|
|
2378
|
+
options.scopes,
|
|
2379
|
+
signal,
|
|
2380
|
+
sessionId
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
async function finalizeSchemaIngest(client, sessionId, options, signal) {
|
|
2384
|
+
const tenantId = resolveTenantId4(client, options.tenantId);
|
|
2385
|
+
return await client.post(
|
|
2386
|
+
sessionPath(sessionId, "/finalize"),
|
|
2387
|
+
{},
|
|
2388
|
+
tenantId,
|
|
2389
|
+
options.userId,
|
|
2390
|
+
options.scopes,
|
|
2391
|
+
signal,
|
|
2392
|
+
sessionId
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
async function syncSchemaInBatches(client, queryEngine, databaseName, options, signal) {
|
|
2396
|
+
const staged = await stageSchemaIngest(
|
|
2397
|
+
client,
|
|
2398
|
+
queryEngine,
|
|
2399
|
+
databaseName,
|
|
2400
|
+
options,
|
|
2401
|
+
signal
|
|
2402
|
+
);
|
|
2403
|
+
if (staged.status === "skipped") {
|
|
2404
|
+
return staged.skippedResult;
|
|
2405
|
+
}
|
|
2406
|
+
const emit = options.onProgress ?? (() => {
|
|
2407
|
+
});
|
|
2408
|
+
const requestOptions = {
|
|
2409
|
+
tenantId: options.tenantId,
|
|
2410
|
+
userId: options.userId,
|
|
2411
|
+
scopes: options.scopes
|
|
2412
|
+
};
|
|
2413
|
+
let chunks = 0;
|
|
2414
|
+
for (let index = 0; index < staged.totalBatches; index += 1) {
|
|
2415
|
+
const embedded = await embedSchemaIngestBatch(
|
|
2416
|
+
client,
|
|
2417
|
+
staged.sessionId,
|
|
2418
|
+
index,
|
|
2419
|
+
requestOptions,
|
|
2420
|
+
signal
|
|
2421
|
+
);
|
|
2422
|
+
chunks += embedded.chunks ?? 0;
|
|
2423
|
+
emit({
|
|
2424
|
+
phase: "embedding",
|
|
2425
|
+
done: index + 1,
|
|
2426
|
+
total: staged.totalBatches,
|
|
2427
|
+
chunks
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
emit({ phase: "finalizing" });
|
|
2431
|
+
return await finalizeSchemaIngest(
|
|
2432
|
+
client,
|
|
2433
|
+
staged.sessionId,
|
|
2434
|
+
requestOptions,
|
|
2435
|
+
signal
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2007
2438
|
function resolveTenantId4(client, tenantId) {
|
|
2008
2439
|
const resolved = tenantId ?? client.getDefaultTenantId();
|
|
2009
2440
|
if (!resolved) {
|
|
@@ -2997,6 +3428,33 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2997
3428
|
const adapter = this.queryEngine.getDatabase(databaseName);
|
|
2998
3429
|
return await adapter.introspect(tables ? { tables } : void 0);
|
|
2999
3430
|
}
|
|
3431
|
+
/**
|
|
3432
|
+
* Lists schemas (ClickHouse: the database, BigQuery: the dataset) with table
|
|
3433
|
+
* counts. Cheap catalog read for building an allowed-tables picker; not
|
|
3434
|
+
* restricted by `allowedTables`.
|
|
3435
|
+
*/
|
|
3436
|
+
async listSchemas(databaseName) {
|
|
3437
|
+
const adapter = this.queryEngine.getDatabase(databaseName);
|
|
3438
|
+
if (!adapter.listSchemas) {
|
|
3439
|
+
throw new Error(
|
|
3440
|
+
`Database "${databaseName}" (${adapter.getDialect()}) does not support listSchemas`
|
|
3441
|
+
);
|
|
3442
|
+
}
|
|
3443
|
+
return await adapter.listSchemas();
|
|
3444
|
+
}
|
|
3445
|
+
/**
|
|
3446
|
+
* Lists tables/views (no columns) filtered by schema and a case-insensitive
|
|
3447
|
+
* name substring. Default limit 500, max 5000. Not restricted by `allowedTables`.
|
|
3448
|
+
*/
|
|
3449
|
+
async listTables(databaseName, options) {
|
|
3450
|
+
const adapter = this.queryEngine.getDatabase(databaseName);
|
|
3451
|
+
if (!adapter.listTables) {
|
|
3452
|
+
throw new Error(
|
|
3453
|
+
`Database "${databaseName}" (${adapter.getDialect()}) does not support listTables`
|
|
3454
|
+
);
|
|
3455
|
+
}
|
|
3456
|
+
return await adapter.listTables(options);
|
|
3457
|
+
}
|
|
3000
3458
|
/**
|
|
3001
3459
|
* Syncs the database schema to QueryPanel for natural language query generation.
|
|
3002
3460
|
*
|
|
@@ -3030,6 +3488,62 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
3030
3488
|
signal
|
|
3031
3489
|
);
|
|
3032
3490
|
}
|
|
3491
|
+
/**
|
|
3492
|
+
* Introspects the schema, uploads it in batches to a new ingest session and
|
|
3493
|
+
* commits it. Returns `status: "skipped"` (with `skippedResult`) when the schema
|
|
3494
|
+
* is unchanged, otherwise `status: "embedding"`: call embedSchemaIngestBatch for
|
|
3495
|
+
* every index in [0, totalBatches) and then finalizeSchemaIngest.
|
|
3496
|
+
* Throws SchemaTooLargeError before uploading if `maxColumns` is exceeded.
|
|
3497
|
+
*/
|
|
3498
|
+
async stageSchemaIngest(databaseName, options, signal) {
|
|
3499
|
+
return await stageSchemaIngest(
|
|
3500
|
+
this.client,
|
|
3501
|
+
this.queryEngine,
|
|
3502
|
+
databaseName,
|
|
3503
|
+
options,
|
|
3504
|
+
signal
|
|
3505
|
+
);
|
|
3506
|
+
}
|
|
3507
|
+
/** Embeds one batch of a staged ingest session. */
|
|
3508
|
+
async embedSchemaIngestBatch(sessionId, index, options, signal) {
|
|
3509
|
+
return await embedSchemaIngestBatch(
|
|
3510
|
+
this.client,
|
|
3511
|
+
sessionId,
|
|
3512
|
+
index,
|
|
3513
|
+
options,
|
|
3514
|
+
signal
|
|
3515
|
+
);
|
|
3516
|
+
}
|
|
3517
|
+
/** Finalizes a staged ingest session after all batches are embedded. */
|
|
3518
|
+
async finalizeSchemaIngest(sessionId, options, signal) {
|
|
3519
|
+
return await finalizeSchemaIngest(
|
|
3520
|
+
this.client,
|
|
3521
|
+
sessionId,
|
|
3522
|
+
options,
|
|
3523
|
+
signal
|
|
3524
|
+
);
|
|
3525
|
+
}
|
|
3526
|
+
/**
|
|
3527
|
+
* Batched alternative to syncSchema for large databases: stage, embed every
|
|
3528
|
+
* batch, and finalize in-process, reporting progress via `onProgress`.
|
|
3529
|
+
*
|
|
3530
|
+
* @example
|
|
3531
|
+
* ```typescript
|
|
3532
|
+
* await qp.syncSchemaInBatches("warehouse", {
|
|
3533
|
+
* tenantId: "tenant_123",
|
|
3534
|
+
* onProgress: (e) => console.log(e.phase, e),
|
|
3535
|
+
* });
|
|
3536
|
+
* ```
|
|
3537
|
+
*/
|
|
3538
|
+
async syncSchemaInBatches(databaseName, options, signal) {
|
|
3539
|
+
return await syncSchemaInBatches(
|
|
3540
|
+
this.client,
|
|
3541
|
+
this.queryEngine,
|
|
3542
|
+
databaseName,
|
|
3543
|
+
options,
|
|
3544
|
+
signal
|
|
3545
|
+
);
|
|
3546
|
+
}
|
|
3033
3547
|
// Natural language query
|
|
3034
3548
|
/**
|
|
3035
3549
|
* Generates SQL from a natural language question and executes it.
|
|
@@ -3146,7 +3660,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
3146
3660
|
* @param input - VizSpec generation input with question, SQL, and result data
|
|
3147
3661
|
* @param options - Optional settings for tenant and retries
|
|
3148
3662
|
* @param signal - Optional AbortSignal for cancellation
|
|
3149
|
-
* @returns VizSpec specification for chart, table, or
|
|
3663
|
+
* @returns VizSpec specification for chart, table, metric, or map visualization
|
|
3150
3664
|
*
|
|
3151
3665
|
* @example
|
|
3152
3666
|
* ```typescript
|
|
@@ -3928,6 +4442,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
3928
4442
|
QueryErrorCode,
|
|
3929
4443
|
QueryPanelSdkAPI,
|
|
3930
4444
|
QueryPipelineError,
|
|
4445
|
+
SchemaTooLargeError,
|
|
3931
4446
|
anonymizeResults,
|
|
3932
4447
|
mapGeneratedParams,
|
|
3933
4448
|
normalizeSqlParamsInput,
|