@querypanel/node-sdk 1.3.5 → 1.3.6
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 +524 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +158 -2
- package/dist/index.d.ts +158 -2
- package/dist/index.js +523 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
// src/adapters/types.ts
|
|
2
|
+
var DEFAULT_CATALOG_LIMIT = 500;
|
|
3
|
+
var MAX_CATALOG_LIMIT = 5e3;
|
|
4
|
+
function resolveCatalogLimit(limit) {
|
|
5
|
+
if (limit === void 0 || !Number.isFinite(limit)) {
|
|
6
|
+
return DEFAULT_CATALOG_LIMIT;
|
|
7
|
+
}
|
|
8
|
+
return Math.min(Math.max(Math.floor(limit), 1), MAX_CATALOG_LIMIT);
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
// src/adapters/bigquery.ts
|
|
2
12
|
var BigQueryAdapter = class {
|
|
3
13
|
constructor(clientFn, options) {
|
|
@@ -106,6 +116,53 @@ var BigQueryAdapter = class {
|
|
|
106
116
|
introspectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
107
117
|
};
|
|
108
118
|
}
|
|
119
|
+
/** The adapter is bound to one dataset, so this returns a single row. */
|
|
120
|
+
async listSchemas() {
|
|
121
|
+
const result = await this.clientFn({
|
|
122
|
+
query: `SELECT COUNT(*) AS table_count
|
|
123
|
+
FROM ${this.informationSchema("TABLES")}`,
|
|
124
|
+
dryRun: false
|
|
125
|
+
});
|
|
126
|
+
return [
|
|
127
|
+
{
|
|
128
|
+
schema: this.defaultDataset,
|
|
129
|
+
tableCount: Number(result.rows[0]?.table_count ?? 0)
|
|
130
|
+
}
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* List tables in the bound dataset. Reads the full catalog (not restricted
|
|
135
|
+
* by `allowedTables`). Default limit 500, max 5000.
|
|
136
|
+
*/
|
|
137
|
+
async listTables(options = {}) {
|
|
138
|
+
if (options.schema && options.schema !== this.defaultDataset) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
const search = options.search?.trim();
|
|
142
|
+
const params = {};
|
|
143
|
+
let searchClause = "";
|
|
144
|
+
if (search) {
|
|
145
|
+
params.search = `%${search.toLowerCase().replace(/[\\%_]/g, (ch) => `\\${ch}`)}%`;
|
|
146
|
+
searchClause = "\n WHERE LOWER(table_name) LIKE @search";
|
|
147
|
+
}
|
|
148
|
+
const limit = resolveCatalogLimit(options.limit);
|
|
149
|
+
const result = await this.clientFn({
|
|
150
|
+
query: `SELECT table_schema, table_name, table_type
|
|
151
|
+
FROM ${this.informationSchema("TABLES")}${searchClause}
|
|
152
|
+
ORDER BY table_name
|
|
153
|
+
LIMIT ${limit}`,
|
|
154
|
+
...search ? { params } : {},
|
|
155
|
+
dryRun: false
|
|
156
|
+
});
|
|
157
|
+
return result.rows.map((row) => ({
|
|
158
|
+
schema: row.table_schema,
|
|
159
|
+
name: row.table_name,
|
|
160
|
+
type: asTableType(row.table_type)
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
informationSchema(view) {
|
|
164
|
+
return `\`${this.datasetProjectId}.${this.defaultDataset}.INFORMATION_SCHEMA.${view}\``;
|
|
165
|
+
}
|
|
109
166
|
validateQueryTables(sql) {
|
|
110
167
|
if (!this.allowedTables || this.allowedTables.length === 0) {
|
|
111
168
|
return;
|
|
@@ -136,8 +193,10 @@ function normalizeTableFilter(tables, defaultDataset) {
|
|
|
136
193
|
for (const raw of tables) {
|
|
137
194
|
if (!raw?.trim()) continue;
|
|
138
195
|
const trimmed = raw.trim();
|
|
196
|
+
if (trimmed === "*" || trimmed === `${defaultDataset}.*`) return [];
|
|
139
197
|
const parts = trimmed.split(".");
|
|
140
198
|
const table = parts.pop() ?? "";
|
|
199
|
+
if (table === "*") continue;
|
|
141
200
|
const dataset = parts.pop() ?? defaultDataset;
|
|
142
201
|
const key = tableKey(dataset, table);
|
|
143
202
|
if (seen.has(key)) continue;
|
|
@@ -248,7 +307,10 @@ var ClickHouseAdapter = class {
|
|
|
248
307
|
this.defaultFormat = options.defaultFormat ?? "JSONEachRow";
|
|
249
308
|
this.kind = options.kind ?? "clickhouse";
|
|
250
309
|
if (options.allowedTables) {
|
|
251
|
-
this.allowedTables = normalizeTableFilter2(
|
|
310
|
+
this.allowedTables = normalizeTableFilter2(
|
|
311
|
+
options.allowedTables,
|
|
312
|
+
this.databaseName
|
|
313
|
+
);
|
|
252
314
|
}
|
|
253
315
|
}
|
|
254
316
|
clientFn;
|
|
@@ -287,7 +349,7 @@ var ClickHouseAdapter = class {
|
|
|
287
349
|
* No indexes, constraints, or statistics
|
|
288
350
|
*/
|
|
289
351
|
async introspect(options) {
|
|
290
|
-
const tablesToIntrospect = options?.tables ? normalizeTableFilter2(options.tables) : this.allowedTables;
|
|
352
|
+
const tablesToIntrospect = options?.tables ? normalizeTableFilter2(options.tables, this.databaseName) : this.allowedTables;
|
|
291
353
|
const allowTables = tablesToIntrospect ?? [];
|
|
292
354
|
const hasFilter = allowTables.length > 0;
|
|
293
355
|
const queryParams = {
|
|
@@ -345,6 +407,53 @@ var ClickHouseAdapter = class {
|
|
|
345
407
|
introspectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
346
408
|
};
|
|
347
409
|
}
|
|
410
|
+
/** The adapter is bound to one database, so this returns a single row. */
|
|
411
|
+
async listSchemas() {
|
|
412
|
+
const rows = await this.query(
|
|
413
|
+
`SELECT count() AS table_count
|
|
414
|
+
FROM system.tables
|
|
415
|
+
WHERE database = {db:String}`,
|
|
416
|
+
{ params: { db: this.databaseName } }
|
|
417
|
+
);
|
|
418
|
+
return [
|
|
419
|
+
{
|
|
420
|
+
schema: this.databaseName,
|
|
421
|
+
tableCount: toNumber(rows[0]?.table_count) ?? 0
|
|
422
|
+
}
|
|
423
|
+
];
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* List tables in the bound database. Reads the full catalog (not restricted
|
|
427
|
+
* by `allowedTables`). Default limit 500, max 5000.
|
|
428
|
+
*/
|
|
429
|
+
async listTables(options = {}) {
|
|
430
|
+
if (options.schema && options.schema !== this.databaseName) {
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
const params = {
|
|
434
|
+
db: this.databaseName,
|
|
435
|
+
limit: resolveCatalogLimit(options.limit)
|
|
436
|
+
};
|
|
437
|
+
const search = options.search?.trim();
|
|
438
|
+
let searchClause = "";
|
|
439
|
+
if (search) {
|
|
440
|
+
params.search = search;
|
|
441
|
+
searchClause = " AND positionCaseInsensitive(name, {search:String}) > 0";
|
|
442
|
+
}
|
|
443
|
+
const rows = await this.query(
|
|
444
|
+
`SELECT name, engine
|
|
445
|
+
FROM system.tables
|
|
446
|
+
WHERE database = {db:String}${searchClause}
|
|
447
|
+
ORDER BY name
|
|
448
|
+
LIMIT {limit:UInt32}`,
|
|
449
|
+
{ params }
|
|
450
|
+
);
|
|
451
|
+
return rows.map((row) => ({
|
|
452
|
+
schema: this.databaseName,
|
|
453
|
+
name: row.name,
|
|
454
|
+
type: asCatalogTableType(row.engine)
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
348
457
|
validateQueryTables(sql) {
|
|
349
458
|
if (!this.allowedTables || this.allowedTables.length === 0) {
|
|
350
459
|
return;
|
|
@@ -410,7 +519,7 @@ function normalizePayload(payload) {
|
|
|
410
519
|
}
|
|
411
520
|
return [];
|
|
412
521
|
}
|
|
413
|
-
function normalizeTableFilter2(tables) {
|
|
522
|
+
function normalizeTableFilter2(tables, databaseName) {
|
|
414
523
|
if (!tables?.length) return [];
|
|
415
524
|
const seen = /* @__PURE__ */ new Set();
|
|
416
525
|
const normalized = [];
|
|
@@ -418,8 +527,10 @@ function normalizeTableFilter2(tables) {
|
|
|
418
527
|
if (!table) continue;
|
|
419
528
|
const trimmed = table.trim();
|
|
420
529
|
if (!trimmed) continue;
|
|
530
|
+
if (trimmed === "*" || trimmed === `${databaseName}.*`) return [];
|
|
421
531
|
const parts = trimmed.split(".");
|
|
422
532
|
const tableName = parts[parts.length - 1];
|
|
533
|
+
if (tableName === "*") continue;
|
|
423
534
|
if (!tableName || seen.has(tableName)) continue;
|
|
424
535
|
seen.add(tableName);
|
|
425
536
|
normalized.push(tableName);
|
|
@@ -452,6 +563,12 @@ function asTableType2(engine) {
|
|
|
452
563
|
}
|
|
453
564
|
return "table";
|
|
454
565
|
}
|
|
566
|
+
function asCatalogTableType(engine) {
|
|
567
|
+
const normalized = typeof engine === "string" ? engine.toLowerCase() : "";
|
|
568
|
+
if (normalized === "materializedview") return "materialized_view";
|
|
569
|
+
if (normalized.includes("view")) return "view";
|
|
570
|
+
return "table";
|
|
571
|
+
}
|
|
455
572
|
function sanitize(value) {
|
|
456
573
|
if (value === null || value === void 0) return void 0;
|
|
457
574
|
const trimmed = String(value).trim();
|
|
@@ -465,6 +582,7 @@ function toNumber(value) {
|
|
|
465
582
|
}
|
|
466
583
|
|
|
467
584
|
// src/adapters/postgres.ts
|
|
585
|
+
var WILDCARD = "*";
|
|
468
586
|
function maxPostgresPlaceholderIndex(sql) {
|
|
469
587
|
let max = 0;
|
|
470
588
|
for (const match of sql.matchAll(/\$(\d+)\b/g)) {
|
|
@@ -536,6 +654,9 @@ var PostgresAdapter = class {
|
|
|
536
654
|
const allowedSet = new Set(
|
|
537
655
|
this.allowedTables.map((t) => tableKey2(t.schema, t.table))
|
|
538
656
|
);
|
|
657
|
+
const allowedSchemas = new Set(
|
|
658
|
+
this.allowedTables.filter((t) => t.table === WILDCARD).map((t) => t.schema)
|
|
659
|
+
);
|
|
539
660
|
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*/)");
|
|
540
661
|
const tablePattern = /(?:FROM|JOIN)\s+(?:ONLY\s+)?(?:([a-zA-Z_][a-zA-Z0-9_]*)\.)?(["']?[a-zA-Z_][a-zA-Z0-9_]*["']?)/gi;
|
|
541
662
|
const matches = neutralizedSql.matchAll(tablePattern);
|
|
@@ -544,7 +665,7 @@ var PostgresAdapter = class {
|
|
|
544
665
|
const table = match[2]?.replace(/['"]/g, "");
|
|
545
666
|
if (table) {
|
|
546
667
|
const key = tableKey2(schema, table);
|
|
547
|
-
if (!allowedSet.has(key)) {
|
|
668
|
+
if (!allowedSet.has(key) && !allowedSchemas.has(schema)) {
|
|
548
669
|
throw new Error(
|
|
549
670
|
`Query references table "${schema}.${table}" which is not in the allowed tables list`
|
|
550
671
|
);
|
|
@@ -596,6 +717,33 @@ var PostgresAdapter = class {
|
|
|
596
717
|
getDialect() {
|
|
597
718
|
return this.dialect;
|
|
598
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* List schemas with their table/view counts. Reads the full catalog (not
|
|
722
|
+
* restricted by `allowedTables`) so it can back an allowed-tables picker.
|
|
723
|
+
*/
|
|
724
|
+
async listSchemas() {
|
|
725
|
+
const result = await this.clientFn(
|
|
726
|
+
this.dialect === "redshift" ? buildRedshiftListSchemasQuery() : buildListSchemasQuery()
|
|
727
|
+
);
|
|
728
|
+
return result.rows.map((row) => ({
|
|
729
|
+
schema: String(row.schema),
|
|
730
|
+
tableCount: Number(row.table_count ?? 0)
|
|
731
|
+
}));
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* List tables/views, optionally filtered by schema and a case-insensitive
|
|
735
|
+
* substring of the table name. Reads the full catalog (not restricted by
|
|
736
|
+
* `allowedTables`). Default limit 500, max 5000.
|
|
737
|
+
*/
|
|
738
|
+
async listTables(options = {}) {
|
|
739
|
+
const { sql, params } = buildListTablesQuery(this.dialect, options);
|
|
740
|
+
const result = await this.clientFn(sql, params);
|
|
741
|
+
return result.rows.map((row) => ({
|
|
742
|
+
schema: String(row.schema),
|
|
743
|
+
name: String(row.name),
|
|
744
|
+
type: asTableType3(String(row.table_type ?? ""))
|
|
745
|
+
}));
|
|
746
|
+
}
|
|
599
747
|
/**
|
|
600
748
|
* Simplified introspection: only collect table/column metadata for IngestRequest
|
|
601
749
|
* No indexes, constraints, or statistics
|
|
@@ -669,7 +817,7 @@ function normalizeTableFilter3(tables, defaultSchema) {
|
|
|
669
817
|
const parts = trimmed.split(".");
|
|
670
818
|
const table = parts.pop() ?? "";
|
|
671
819
|
const schema = parts.pop() ?? defaultSchema;
|
|
672
|
-
if (!isSafeIdentifier(schema) || !isSafeIdentifier(table)) {
|
|
820
|
+
if (!isSafeIdentifier(schema) || table !== WILDCARD && !isSafeIdentifier(table)) {
|
|
673
821
|
continue;
|
|
674
822
|
}
|
|
675
823
|
const key = tableKey2(schema, table);
|
|
@@ -771,11 +919,83 @@ function buildRedshiftColumnsQuery(tables) {
|
|
|
771
919
|
}
|
|
772
920
|
function buildFilterClause(tables, schemaExpr, tableExpr) {
|
|
773
921
|
if (!tables.length) return "";
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
922
|
+
const wildcardSchemas = new Set(
|
|
923
|
+
tables.filter((t) => t.table === WILDCARD).map((t) => t.schema)
|
|
924
|
+
);
|
|
925
|
+
const clauses = [];
|
|
926
|
+
for (const schema of wildcardSchemas) {
|
|
927
|
+
clauses.push(`(${schemaExpr} = '${schema}')`);
|
|
928
|
+
}
|
|
929
|
+
for (const { schema, table } of tables) {
|
|
930
|
+
if (table === WILDCARD || wildcardSchemas.has(schema)) continue;
|
|
931
|
+
clauses.push(`(${schemaExpr} = '${schema}' AND ${tableExpr} = '${table}')`);
|
|
932
|
+
}
|
|
777
933
|
return `AND (${clauses.join(" OR ")})`;
|
|
778
934
|
}
|
|
935
|
+
function buildListSchemasQuery() {
|
|
936
|
+
return `SELECT n.nspname AS schema, COUNT(*) AS table_count
|
|
937
|
+
FROM pg_class c
|
|
938
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
939
|
+
WHERE c.relkind IN ('r', 'v', 'm')
|
|
940
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
941
|
+
AND n.nspname NOT LIKE 'pg_temp%'
|
|
942
|
+
AND n.nspname NOT LIKE 'pg_toast_temp%'
|
|
943
|
+
GROUP BY n.nspname
|
|
944
|
+
ORDER BY n.nspname;`;
|
|
945
|
+
}
|
|
946
|
+
function buildRedshiftListSchemasQuery() {
|
|
947
|
+
return `SELECT table_schema AS schema, COUNT(*) AS table_count
|
|
948
|
+
FROM svv_tables
|
|
949
|
+
WHERE table_catalog = current_database()
|
|
950
|
+
AND table_schema NOT IN ${REDSHIFT_EXCLUDED_SCHEMAS}
|
|
951
|
+
AND table_schema NOT LIKE 'pg_temp%'
|
|
952
|
+
GROUP BY table_schema
|
|
953
|
+
ORDER BY table_schema;`;
|
|
954
|
+
}
|
|
955
|
+
function toLikeSubstringPattern(search) {
|
|
956
|
+
return `%${search.replace(/[\\%_]/g, (ch) => `\\${ch}`)}%`;
|
|
957
|
+
}
|
|
958
|
+
function buildListTablesQuery(dialect, options) {
|
|
959
|
+
const isRedshift = dialect === "redshift";
|
|
960
|
+
const schemaExpr = isRedshift ? "table_schema" : "n.nspname";
|
|
961
|
+
const tableExpr = isRedshift ? "table_name" : "c.relname";
|
|
962
|
+
const params = [];
|
|
963
|
+
const filters = [];
|
|
964
|
+
if (options.schema) {
|
|
965
|
+
params.push(options.schema);
|
|
966
|
+
filters.push(`AND ${schemaExpr} = $${params.length}`);
|
|
967
|
+
}
|
|
968
|
+
const search = options.search?.trim();
|
|
969
|
+
if (search) {
|
|
970
|
+
params.push(toLikeSubstringPattern(search));
|
|
971
|
+
filters.push(`AND ${tableExpr} ILIKE $${params.length}`);
|
|
972
|
+
}
|
|
973
|
+
const limit = resolveCatalogLimit(options.limit);
|
|
974
|
+
const filterSql = filters.join("\n ");
|
|
975
|
+
const sql = isRedshift ? `SELECT table_schema AS schema, table_name AS name, table_type
|
|
976
|
+
FROM svv_tables
|
|
977
|
+
WHERE table_catalog = current_database()
|
|
978
|
+
AND table_schema NOT IN ${REDSHIFT_EXCLUDED_SCHEMAS}
|
|
979
|
+
AND table_schema NOT LIKE 'pg_temp%'
|
|
980
|
+
${filterSql}
|
|
981
|
+
ORDER BY table_schema, table_name
|
|
982
|
+
LIMIT ${limit};` : `SELECT n.nspname AS schema, c.relname AS name,
|
|
983
|
+
CASE c.relkind
|
|
984
|
+
WHEN 'v' THEN 'view'
|
|
985
|
+
WHEN 'm' THEN 'materialized_view'
|
|
986
|
+
ELSE 'table'
|
|
987
|
+
END AS table_type
|
|
988
|
+
FROM pg_class c
|
|
989
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
990
|
+
WHERE c.relkind IN ('r', 'v', 'm')
|
|
991
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
992
|
+
AND n.nspname NOT LIKE 'pg_temp%'
|
|
993
|
+
AND n.nspname NOT LIKE 'pg_toast_temp%'
|
|
994
|
+
${filterSql}
|
|
995
|
+
ORDER BY n.nspname, c.relname
|
|
996
|
+
LIMIT ${limit};`;
|
|
997
|
+
return { sql, params };
|
|
998
|
+
}
|
|
779
999
|
function tableKey2(schema, table) {
|
|
780
1000
|
return `${schema}.${table}`;
|
|
781
1001
|
}
|
|
@@ -910,6 +1130,20 @@ var QueryPipelineError = class extends Error {
|
|
|
910
1130
|
return isAiProviderErrorCode(this.code);
|
|
911
1131
|
}
|
|
912
1132
|
};
|
|
1133
|
+
var SchemaTooLargeError = class extends Error {
|
|
1134
|
+
constructor(tableCount, columnCount, maxColumns) {
|
|
1135
|
+
super(
|
|
1136
|
+
`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).`
|
|
1137
|
+
);
|
|
1138
|
+
this.tableCount = tableCount;
|
|
1139
|
+
this.columnCount = columnCount;
|
|
1140
|
+
this.maxColumns = maxColumns;
|
|
1141
|
+
this.name = "SchemaTooLargeError";
|
|
1142
|
+
}
|
|
1143
|
+
tableCount;
|
|
1144
|
+
columnCount;
|
|
1145
|
+
maxColumns;
|
|
1146
|
+
};
|
|
913
1147
|
|
|
914
1148
|
// src/core/client.ts
|
|
915
1149
|
var ApiClient = class {
|
|
@@ -1049,7 +1283,7 @@ var ApiClient = class {
|
|
|
1049
1283
|
json = void 0;
|
|
1050
1284
|
}
|
|
1051
1285
|
if (!response.ok) {
|
|
1052
|
-
const message = json
|
|
1286
|
+
const message = errorMessageFromBody(json, response);
|
|
1053
1287
|
if (isQueryErrorCode(json?.code)) {
|
|
1054
1288
|
throw new QueryPipelineError(
|
|
1055
1289
|
message,
|
|
@@ -1171,6 +1405,17 @@ var ApiClient = class {
|
|
|
1171
1405
|
return `${data}.${encodedSignature}`;
|
|
1172
1406
|
}
|
|
1173
1407
|
};
|
|
1408
|
+
function errorMessageFromBody(json, response) {
|
|
1409
|
+
const error = json?.error;
|
|
1410
|
+
if (typeof error === "string" && error) return error;
|
|
1411
|
+
if (error && typeof error === "object") {
|
|
1412
|
+
const nested = error.message ?? error.code;
|
|
1413
|
+
if (typeof nested === "string" && nested) {
|
|
1414
|
+
return `${nested} (HTTP ${response.status})`;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return response.statusText || `Request failed (HTTP ${response.status})`;
|
|
1418
|
+
}
|
|
1174
1419
|
|
|
1175
1420
|
// src/core/callback-client.ts
|
|
1176
1421
|
var CallbackApiClient = class {
|
|
@@ -1957,6 +2202,191 @@ async function syncSchema(client, queryEngine, databaseName, options, signal) {
|
|
|
1957
2202
|
);
|
|
1958
2203
|
return response;
|
|
1959
2204
|
}
|
|
2205
|
+
var DEFAULT_MAX_BATCH_DOCS = 1e3;
|
|
2206
|
+
var DEFAULT_MAX_BATCH_BYTES = 2e6;
|
|
2207
|
+
var utf8 = new TextEncoder();
|
|
2208
|
+
function jsonBytes(value) {
|
|
2209
|
+
return utf8.encode(JSON.stringify(value)).length;
|
|
2210
|
+
}
|
|
2211
|
+
var BATCH_ENVELOPE_BYTES = jsonBytes({ tables: [] });
|
|
2212
|
+
function splitIngestTablesIntoBatches(tables, maxBatchDocs = DEFAULT_MAX_BATCH_DOCS, maxBatchBytes = DEFAULT_MAX_BATCH_BYTES) {
|
|
2213
|
+
const batches = [];
|
|
2214
|
+
let current = [];
|
|
2215
|
+
let currentDocs = 0;
|
|
2216
|
+
let currentBytes = BATCH_ENVELOPE_BYTES;
|
|
2217
|
+
for (const table of tables) {
|
|
2218
|
+
const docs = 1 + table.columns.length;
|
|
2219
|
+
const bytes = jsonBytes(table);
|
|
2220
|
+
if (current.length > 0 && (currentDocs + docs > maxBatchDocs || currentBytes + 1 + bytes > maxBatchBytes)) {
|
|
2221
|
+
batches.push(current);
|
|
2222
|
+
current = [];
|
|
2223
|
+
currentDocs = 0;
|
|
2224
|
+
currentBytes = BATCH_ENVELOPE_BYTES;
|
|
2225
|
+
}
|
|
2226
|
+
currentBytes += bytes + (current.length > 0 ? 1 : 0);
|
|
2227
|
+
current.push(table);
|
|
2228
|
+
currentDocs += docs;
|
|
2229
|
+
}
|
|
2230
|
+
if (current.length > 0 || batches.length === 0) {
|
|
2231
|
+
batches.push(current);
|
|
2232
|
+
}
|
|
2233
|
+
return batches;
|
|
2234
|
+
}
|
|
2235
|
+
function sessionPath(sessionId, suffix) {
|
|
2236
|
+
return `/ingest/sessions/${encodeURIComponent(sessionId)}${suffix}`;
|
|
2237
|
+
}
|
|
2238
|
+
async function stageSchemaIngest(client, queryEngine, databaseName, options, signal) {
|
|
2239
|
+
const tenantId = resolveTenantId4(client, options.tenantId);
|
|
2240
|
+
const adapter = queryEngine.getDatabase(databaseName);
|
|
2241
|
+
const metadata = queryEngine.getDatabaseMetadata(databaseName);
|
|
2242
|
+
const emit = options.onProgress ?? (() => {
|
|
2243
|
+
});
|
|
2244
|
+
emit({ phase: "introspecting" });
|
|
2245
|
+
const introspection = await adapter.introspect(
|
|
2246
|
+
options.tables ? { tables: options.tables } : void 0
|
|
2247
|
+
);
|
|
2248
|
+
const { tables, ...sessionFields } = buildSchemaRequest(
|
|
2249
|
+
databaseName,
|
|
2250
|
+
adapter,
|
|
2251
|
+
introspection,
|
|
2252
|
+
metadata
|
|
2253
|
+
);
|
|
2254
|
+
const tableCount = tables.length;
|
|
2255
|
+
const columnCount = tables.reduce((sum, t) => sum + t.columns.length, 0);
|
|
2256
|
+
emit({ phase: "introspected", tableCount, columnCount });
|
|
2257
|
+
if (options.maxColumns !== void 0 && columnCount > options.maxColumns) {
|
|
2258
|
+
throw new SchemaTooLargeError(tableCount, columnCount, options.maxColumns);
|
|
2259
|
+
}
|
|
2260
|
+
const batches = splitIngestTablesIntoBatches(
|
|
2261
|
+
tables,
|
|
2262
|
+
options.maxBatchDocs ?? DEFAULT_MAX_BATCH_DOCS,
|
|
2263
|
+
options.maxBatchBytes ?? DEFAULT_MAX_BATCH_BYTES
|
|
2264
|
+
);
|
|
2265
|
+
const totalBatches = batches.length;
|
|
2266
|
+
const created = await client.post(
|
|
2267
|
+
"/ingest/sessions",
|
|
2268
|
+
{
|
|
2269
|
+
...sessionFields,
|
|
2270
|
+
...options.forceReindex ? { force_reindex: true } : {},
|
|
2271
|
+
total_batches: totalBatches
|
|
2272
|
+
},
|
|
2273
|
+
tenantId,
|
|
2274
|
+
options.userId,
|
|
2275
|
+
options.scopes,
|
|
2276
|
+
signal,
|
|
2277
|
+
crypto2.randomUUID()
|
|
2278
|
+
);
|
|
2279
|
+
const sessionId = created.session_id;
|
|
2280
|
+
for (let index = 0; index < totalBatches; index += 1) {
|
|
2281
|
+
await client.put(
|
|
2282
|
+
sessionPath(sessionId, `/batches/${index}`),
|
|
2283
|
+
{ tables: batches[index] },
|
|
2284
|
+
tenantId,
|
|
2285
|
+
options.userId,
|
|
2286
|
+
options.scopes,
|
|
2287
|
+
signal,
|
|
2288
|
+
sessionId
|
|
2289
|
+
);
|
|
2290
|
+
emit({ phase: "uploading", done: index + 1, total: totalBatches });
|
|
2291
|
+
}
|
|
2292
|
+
emit({ phase: "committing" });
|
|
2293
|
+
const committed = await client.post(
|
|
2294
|
+
sessionPath(sessionId, "/commit"),
|
|
2295
|
+
{},
|
|
2296
|
+
tenantId,
|
|
2297
|
+
options.userId,
|
|
2298
|
+
options.scopes,
|
|
2299
|
+
signal,
|
|
2300
|
+
sessionId
|
|
2301
|
+
);
|
|
2302
|
+
if (committed.status === "skipped") {
|
|
2303
|
+
const { status: _status, ...skippedResult } = committed;
|
|
2304
|
+
return {
|
|
2305
|
+
sessionId,
|
|
2306
|
+
status: "skipped",
|
|
2307
|
+
totalBatches,
|
|
2308
|
+
tableCount,
|
|
2309
|
+
columnCount,
|
|
2310
|
+
skippedResult,
|
|
2311
|
+
driftDetected: skippedResult.drift_detected
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
return {
|
|
2315
|
+
sessionId,
|
|
2316
|
+
status: "embedding",
|
|
2317
|
+
totalBatches: committed.total_batches ?? totalBatches,
|
|
2318
|
+
tableCount,
|
|
2319
|
+
columnCount,
|
|
2320
|
+
driftDetected: committed.drift_detected
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
async function embedSchemaIngestBatch(client, sessionId, index, options, signal) {
|
|
2324
|
+
const tenantId = resolveTenantId4(client, options.tenantId);
|
|
2325
|
+
return await client.post(
|
|
2326
|
+
sessionPath(sessionId, `/batches/${index}/embed`),
|
|
2327
|
+
{},
|
|
2328
|
+
tenantId,
|
|
2329
|
+
options.userId,
|
|
2330
|
+
options.scopes,
|
|
2331
|
+
signal,
|
|
2332
|
+
sessionId
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
async function finalizeSchemaIngest(client, sessionId, options, signal) {
|
|
2336
|
+
const tenantId = resolveTenantId4(client, options.tenantId);
|
|
2337
|
+
return await client.post(
|
|
2338
|
+
sessionPath(sessionId, "/finalize"),
|
|
2339
|
+
{},
|
|
2340
|
+
tenantId,
|
|
2341
|
+
options.userId,
|
|
2342
|
+
options.scopes,
|
|
2343
|
+
signal,
|
|
2344
|
+
sessionId
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
async function syncSchemaInBatches(client, queryEngine, databaseName, options, signal) {
|
|
2348
|
+
const staged = await stageSchemaIngest(
|
|
2349
|
+
client,
|
|
2350
|
+
queryEngine,
|
|
2351
|
+
databaseName,
|
|
2352
|
+
options,
|
|
2353
|
+
signal
|
|
2354
|
+
);
|
|
2355
|
+
if (staged.status === "skipped") {
|
|
2356
|
+
return staged.skippedResult;
|
|
2357
|
+
}
|
|
2358
|
+
const emit = options.onProgress ?? (() => {
|
|
2359
|
+
});
|
|
2360
|
+
const requestOptions = {
|
|
2361
|
+
tenantId: options.tenantId,
|
|
2362
|
+
userId: options.userId,
|
|
2363
|
+
scopes: options.scopes
|
|
2364
|
+
};
|
|
2365
|
+
let chunks = 0;
|
|
2366
|
+
for (let index = 0; index < staged.totalBatches; index += 1) {
|
|
2367
|
+
const embedded = await embedSchemaIngestBatch(
|
|
2368
|
+
client,
|
|
2369
|
+
staged.sessionId,
|
|
2370
|
+
index,
|
|
2371
|
+
requestOptions,
|
|
2372
|
+
signal
|
|
2373
|
+
);
|
|
2374
|
+
chunks += embedded.chunks ?? 0;
|
|
2375
|
+
emit({
|
|
2376
|
+
phase: "embedding",
|
|
2377
|
+
done: index + 1,
|
|
2378
|
+
total: staged.totalBatches,
|
|
2379
|
+
chunks
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
emit({ phase: "finalizing" });
|
|
2383
|
+
return await finalizeSchemaIngest(
|
|
2384
|
+
client,
|
|
2385
|
+
staged.sessionId,
|
|
2386
|
+
requestOptions,
|
|
2387
|
+
signal
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
1960
2390
|
function resolveTenantId4(client, tenantId) {
|
|
1961
2391
|
const resolved = tenantId ?? client.getDefaultTenantId();
|
|
1962
2392
|
if (!resolved) {
|
|
@@ -2950,6 +3380,33 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2950
3380
|
const adapter = this.queryEngine.getDatabase(databaseName);
|
|
2951
3381
|
return await adapter.introspect(tables ? { tables } : void 0);
|
|
2952
3382
|
}
|
|
3383
|
+
/**
|
|
3384
|
+
* Lists schemas (ClickHouse: the database, BigQuery: the dataset) with table
|
|
3385
|
+
* counts. Cheap catalog read for building an allowed-tables picker; not
|
|
3386
|
+
* restricted by `allowedTables`.
|
|
3387
|
+
*/
|
|
3388
|
+
async listSchemas(databaseName) {
|
|
3389
|
+
const adapter = this.queryEngine.getDatabase(databaseName);
|
|
3390
|
+
if (!adapter.listSchemas) {
|
|
3391
|
+
throw new Error(
|
|
3392
|
+
`Database "${databaseName}" (${adapter.getDialect()}) does not support listSchemas`
|
|
3393
|
+
);
|
|
3394
|
+
}
|
|
3395
|
+
return await adapter.listSchemas();
|
|
3396
|
+
}
|
|
3397
|
+
/**
|
|
3398
|
+
* Lists tables/views (no columns) filtered by schema and a case-insensitive
|
|
3399
|
+
* name substring. Default limit 500, max 5000. Not restricted by `allowedTables`.
|
|
3400
|
+
*/
|
|
3401
|
+
async listTables(databaseName, options) {
|
|
3402
|
+
const adapter = this.queryEngine.getDatabase(databaseName);
|
|
3403
|
+
if (!adapter.listTables) {
|
|
3404
|
+
throw new Error(
|
|
3405
|
+
`Database "${databaseName}" (${adapter.getDialect()}) does not support listTables`
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
return await adapter.listTables(options);
|
|
3409
|
+
}
|
|
2953
3410
|
/**
|
|
2954
3411
|
* Syncs the database schema to QueryPanel for natural language query generation.
|
|
2955
3412
|
*
|
|
@@ -2983,6 +3440,62 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2983
3440
|
signal
|
|
2984
3441
|
);
|
|
2985
3442
|
}
|
|
3443
|
+
/**
|
|
3444
|
+
* Introspects the schema, uploads it in batches to a new ingest session and
|
|
3445
|
+
* commits it. Returns `status: "skipped"` (with `skippedResult`) when the schema
|
|
3446
|
+
* is unchanged, otherwise `status: "embedding"`: call embedSchemaIngestBatch for
|
|
3447
|
+
* every index in [0, totalBatches) and then finalizeSchemaIngest.
|
|
3448
|
+
* Throws SchemaTooLargeError before uploading if `maxColumns` is exceeded.
|
|
3449
|
+
*/
|
|
3450
|
+
async stageSchemaIngest(databaseName, options, signal) {
|
|
3451
|
+
return await stageSchemaIngest(
|
|
3452
|
+
this.client,
|
|
3453
|
+
this.queryEngine,
|
|
3454
|
+
databaseName,
|
|
3455
|
+
options,
|
|
3456
|
+
signal
|
|
3457
|
+
);
|
|
3458
|
+
}
|
|
3459
|
+
/** Embeds one batch of a staged ingest session. */
|
|
3460
|
+
async embedSchemaIngestBatch(sessionId, index, options, signal) {
|
|
3461
|
+
return await embedSchemaIngestBatch(
|
|
3462
|
+
this.client,
|
|
3463
|
+
sessionId,
|
|
3464
|
+
index,
|
|
3465
|
+
options,
|
|
3466
|
+
signal
|
|
3467
|
+
);
|
|
3468
|
+
}
|
|
3469
|
+
/** Finalizes a staged ingest session after all batches are embedded. */
|
|
3470
|
+
async finalizeSchemaIngest(sessionId, options, signal) {
|
|
3471
|
+
return await finalizeSchemaIngest(
|
|
3472
|
+
this.client,
|
|
3473
|
+
sessionId,
|
|
3474
|
+
options,
|
|
3475
|
+
signal
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
/**
|
|
3479
|
+
* Batched alternative to syncSchema for large databases: stage, embed every
|
|
3480
|
+
* batch, and finalize in-process, reporting progress via `onProgress`.
|
|
3481
|
+
*
|
|
3482
|
+
* @example
|
|
3483
|
+
* ```typescript
|
|
3484
|
+
* await qp.syncSchemaInBatches("warehouse", {
|
|
3485
|
+
* tenantId: "tenant_123",
|
|
3486
|
+
* onProgress: (e) => console.log(e.phase, e),
|
|
3487
|
+
* });
|
|
3488
|
+
* ```
|
|
3489
|
+
*/
|
|
3490
|
+
async syncSchemaInBatches(databaseName, options, signal) {
|
|
3491
|
+
return await syncSchemaInBatches(
|
|
3492
|
+
this.client,
|
|
3493
|
+
this.queryEngine,
|
|
3494
|
+
databaseName,
|
|
3495
|
+
options,
|
|
3496
|
+
signal
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
2986
3499
|
// Natural language query
|
|
2987
3500
|
/**
|
|
2988
3501
|
* Generates SQL from a natural language question and executes it.
|
|
@@ -3880,6 +4393,7 @@ export {
|
|
|
3880
4393
|
QueryErrorCode,
|
|
3881
4394
|
QueryPanelSdkAPI,
|
|
3882
4395
|
QueryPipelineError,
|
|
4396
|
+
SchemaTooLargeError,
|
|
3883
4397
|
anonymizeResults,
|
|
3884
4398
|
mapGeneratedParams,
|
|
3885
4399
|
normalizeSqlParamsInput,
|