@querypanel/node-sdk 1.3.3 → 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 +71 -19
- package/dist/index.cjs +574 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +180 -5
- package/dist/index.d.ts +180 -5
- package/dist/index.js +573 -15
- package/dist/index.js.map +1 -1
- package/package.json +16 -3
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)) {
|
|
@@ -548,7 +667,8 @@ var PostgresAdapter = class {
|
|
|
548
667
|
this.clientFn = clientFn;
|
|
549
668
|
this.databaseName = options.database ?? "postgres";
|
|
550
669
|
this.defaultSchema = options.defaultSchema ?? "public";
|
|
551
|
-
this.
|
|
670
|
+
this.dialect = options.dialect ?? "postgres";
|
|
671
|
+
this.kind = options.kind ?? this.dialect;
|
|
552
672
|
if (options.allowedTables) {
|
|
553
673
|
this.allowedTables = normalizeTableFilter3(
|
|
554
674
|
options.allowedTables,
|
|
@@ -560,6 +680,7 @@ var PostgresAdapter = class {
|
|
|
560
680
|
databaseName;
|
|
561
681
|
defaultSchema;
|
|
562
682
|
kind;
|
|
683
|
+
dialect;
|
|
563
684
|
allowedTables;
|
|
564
685
|
async execute(sql, params) {
|
|
565
686
|
if (this.allowedTables) {
|
|
@@ -581,6 +702,9 @@ var PostgresAdapter = class {
|
|
|
581
702
|
const allowedSet = new Set(
|
|
582
703
|
this.allowedTables.map((t) => tableKey2(t.schema, t.table))
|
|
583
704
|
);
|
|
705
|
+
const allowedSchemas = new Set(
|
|
706
|
+
this.allowedTables.filter((t) => t.table === WILDCARD).map((t) => t.schema)
|
|
707
|
+
);
|
|
584
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*/)");
|
|
585
709
|
const tablePattern = /(?:FROM|JOIN)\s+(?:ONLY\s+)?(?:([a-zA-Z_][a-zA-Z0-9_]*)\.)?(["']?[a-zA-Z_][a-zA-Z0-9_]*["']?)/gi;
|
|
586
710
|
const matches = neutralizedSql.matchAll(tablePattern);
|
|
@@ -589,7 +713,7 @@ var PostgresAdapter = class {
|
|
|
589
713
|
const table = match[2]?.replace(/['"]/g, "");
|
|
590
714
|
if (table) {
|
|
591
715
|
const key = tableKey2(schema, table);
|
|
592
|
-
if (!allowedSet.has(key)) {
|
|
716
|
+
if (!allowedSet.has(key) && !allowedSchemas.has(schema)) {
|
|
593
717
|
throw new Error(
|
|
594
718
|
`Query references table "${schema}.${table}" which is not in the allowed tables list`
|
|
595
719
|
);
|
|
@@ -639,7 +763,34 @@ var PostgresAdapter = class {
|
|
|
639
763
|
await this.clientFn(`EXPLAIN ${sql}`, paramArray);
|
|
640
764
|
}
|
|
641
765
|
getDialect() {
|
|
642
|
-
return
|
|
766
|
+
return this.dialect;
|
|
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
|
+
}));
|
|
643
794
|
}
|
|
644
795
|
/**
|
|
645
796
|
* Simplified introspection: only collect table/column metadata for IngestRequest
|
|
@@ -648,12 +799,13 @@ var PostgresAdapter = class {
|
|
|
648
799
|
async introspect(options) {
|
|
649
800
|
const tablesToIntrospect = options?.tables ? normalizeTableFilter3(options.tables, this.defaultSchema) : this.allowedTables;
|
|
650
801
|
const normalizedTables = tablesToIntrospect ?? [];
|
|
802
|
+
const isRedshift = this.dialect === "redshift";
|
|
651
803
|
const tablesResult = await this.clientFn(
|
|
652
|
-
buildTablesQuery2(normalizedTables)
|
|
804
|
+
isRedshift ? buildRedshiftTablesQuery(normalizedTables) : buildTablesQuery2(normalizedTables)
|
|
653
805
|
);
|
|
654
806
|
const tableRows = tablesResult.rows;
|
|
655
807
|
const columnsResult = await this.clientFn(
|
|
656
|
-
buildColumnsQuery2(normalizedTables)
|
|
808
|
+
isRedshift ? buildRedshiftColumnsQuery(normalizedTables) : buildColumnsQuery2(normalizedTables)
|
|
657
809
|
);
|
|
658
810
|
const columnRows = columnsResult.rows;
|
|
659
811
|
const tablesByKey = /* @__PURE__ */ new Map();
|
|
@@ -713,7 +865,7 @@ function normalizeTableFilter3(tables, defaultSchema) {
|
|
|
713
865
|
const parts = trimmed.split(".");
|
|
714
866
|
const table = parts.pop() ?? "";
|
|
715
867
|
const schema = parts.pop() ?? defaultSchema;
|
|
716
|
-
if (!isSafeIdentifier(schema) || !isSafeIdentifier(table)) {
|
|
868
|
+
if (!isSafeIdentifier(schema) || table !== WILDCARD && !isSafeIdentifier(table)) {
|
|
717
869
|
continue;
|
|
718
870
|
}
|
|
719
871
|
const key = tableKey2(schema, table);
|
|
@@ -767,10 +919,11 @@ function buildColumnsQuery2(tables) {
|
|
|
767
919
|
AND kcu.column_name = cols.column_name
|
|
768
920
|
) AS is_primary_key
|
|
769
921
|
FROM information_schema.columns cols
|
|
922
|
+
LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = cols.table_schema
|
|
770
923
|
LEFT JOIN pg_catalog.pg_class c
|
|
771
924
|
ON c.relname = cols.table_name
|
|
925
|
+
AND c.relnamespace = n.oid
|
|
772
926
|
AND c.relkind IN ('r', 'v', 'm')
|
|
773
|
-
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
774
927
|
LEFT JOIN pg_catalog.pg_attribute attr
|
|
775
928
|
ON attr.attrelid = c.oid
|
|
776
929
|
AND attr.attname = cols.column_name
|
|
@@ -780,13 +933,117 @@ function buildColumnsQuery2(tables) {
|
|
|
780
933
|
${filter}
|
|
781
934
|
ORDER BY cols.table_schema, cols.table_name, cols.ordinal_position;`;
|
|
782
935
|
}
|
|
936
|
+
var REDSHIFT_EXCLUDED_SCHEMAS = "('pg_catalog', 'information_schema', 'pg_internal', 'pg_automv')";
|
|
937
|
+
function buildRedshiftTablesQuery(tables) {
|
|
938
|
+
const filter = buildFilterClause(tables, "table_schema", "table_name");
|
|
939
|
+
return `SELECT
|
|
940
|
+
table_name,
|
|
941
|
+
table_schema AS schema_name,
|
|
942
|
+
table_type,
|
|
943
|
+
remarks AS comment
|
|
944
|
+
FROM svv_tables
|
|
945
|
+
WHERE table_catalog = current_database()
|
|
946
|
+
AND table_schema NOT IN ${REDSHIFT_EXCLUDED_SCHEMAS}
|
|
947
|
+
AND table_schema NOT LIKE 'pg_temp%'
|
|
948
|
+
${filter}
|
|
949
|
+
ORDER BY table_schema, table_name;`;
|
|
950
|
+
}
|
|
951
|
+
function buildRedshiftColumnsQuery(tables) {
|
|
952
|
+
const filter = buildFilterClause(tables, "table_schema", "table_name");
|
|
953
|
+
return `SELECT
|
|
954
|
+
table_name,
|
|
955
|
+
table_schema,
|
|
956
|
+
column_name,
|
|
957
|
+
data_type,
|
|
958
|
+
NULL AS udt_name,
|
|
959
|
+
remarks AS description,
|
|
960
|
+
FALSE AS is_primary_key
|
|
961
|
+
FROM svv_columns
|
|
962
|
+
WHERE table_catalog = current_database()
|
|
963
|
+
AND table_schema NOT IN ${REDSHIFT_EXCLUDED_SCHEMAS}
|
|
964
|
+
AND table_schema NOT LIKE 'pg_temp%'
|
|
965
|
+
${filter}
|
|
966
|
+
ORDER BY table_schema, table_name, ordinal_position;`;
|
|
967
|
+
}
|
|
783
968
|
function buildFilterClause(tables, schemaExpr, tableExpr) {
|
|
784
969
|
if (!tables.length) return "";
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
|
|
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
|
+
}
|
|
788
981
|
return `AND (${clauses.join(" OR ")})`;
|
|
789
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
|
+
}
|
|
790
1047
|
function tableKey2(schema, table) {
|
|
791
1048
|
return `${schema}.${table}`;
|
|
792
1049
|
}
|
|
@@ -921,6 +1178,20 @@ var QueryPipelineError = class extends Error {
|
|
|
921
1178
|
return isAiProviderErrorCode(this.code);
|
|
922
1179
|
}
|
|
923
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
|
+
};
|
|
924
1195
|
|
|
925
1196
|
// src/core/client.ts
|
|
926
1197
|
var ApiClient = class {
|
|
@@ -1060,7 +1331,7 @@ var ApiClient = class {
|
|
|
1060
1331
|
json = void 0;
|
|
1061
1332
|
}
|
|
1062
1333
|
if (!response.ok) {
|
|
1063
|
-
const message = json
|
|
1334
|
+
const message = errorMessageFromBody(json, response);
|
|
1064
1335
|
if (isQueryErrorCode(json?.code)) {
|
|
1065
1336
|
throw new QueryPipelineError(
|
|
1066
1337
|
message,
|
|
@@ -1182,6 +1453,17 @@ var ApiClient = class {
|
|
|
1182
1453
|
return `${data}.${encodedSignature}`;
|
|
1183
1454
|
}
|
|
1184
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
|
+
}
|
|
1185
1467
|
|
|
1186
1468
|
// src/core/callback-client.ts
|
|
1187
1469
|
var CallbackApiClient = class {
|
|
@@ -1968,6 +2250,191 @@ async function syncSchema(client, queryEngine, databaseName, options, signal) {
|
|
|
1968
2250
|
);
|
|
1969
2251
|
return response;
|
|
1970
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
|
+
}
|
|
1971
2438
|
function resolveTenantId4(client, tenantId) {
|
|
1972
2439
|
const resolved = tenantId ?? client.getDefaultTenantId();
|
|
1973
2440
|
if (!resolved) {
|
|
@@ -2913,7 +3380,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2913
3380
|
const adapter = new PostgresAdapter(clientFn, options);
|
|
2914
3381
|
const metadata = {
|
|
2915
3382
|
name,
|
|
2916
|
-
dialect:
|
|
3383
|
+
dialect: adapter.getDialect(),
|
|
2917
3384
|
description: options?.description,
|
|
2918
3385
|
tags: options?.tags,
|
|
2919
3386
|
tenantFieldName: options?.tenantFieldName,
|
|
@@ -2925,6 +3392,14 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2925
3392
|
};
|
|
2926
3393
|
this.queryEngine.attachDatabase(name, adapter, metadata);
|
|
2927
3394
|
}
|
|
3395
|
+
/**
|
|
3396
|
+
* Attach an Amazon Redshift database. Redshift speaks the Postgres wire
|
|
3397
|
+
* protocol, so `clientFn` can wrap a `pg` client; introspection uses
|
|
3398
|
+
* Redshift's svv_* catalog views and SQL is generated for Redshift.
|
|
3399
|
+
*/
|
|
3400
|
+
attachRedshift(name, clientFn, options) {
|
|
3401
|
+
this.attachPostgres(name, clientFn, { ...options, dialect: "redshift" });
|
|
3402
|
+
}
|
|
2928
3403
|
attachBigQuery(name, clientFn, options) {
|
|
2929
3404
|
const adapter = new BigQueryAdapter(clientFn, options);
|
|
2930
3405
|
const metadata = {
|
|
@@ -2953,6 +3428,33 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2953
3428
|
const adapter = this.queryEngine.getDatabase(databaseName);
|
|
2954
3429
|
return await adapter.introspect(tables ? { tables } : void 0);
|
|
2955
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
|
+
}
|
|
2956
3458
|
/**
|
|
2957
3459
|
* Syncs the database schema to QueryPanel for natural language query generation.
|
|
2958
3460
|
*
|
|
@@ -2986,6 +3488,62 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2986
3488
|
signal
|
|
2987
3489
|
);
|
|
2988
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
|
+
}
|
|
2989
3547
|
// Natural language query
|
|
2990
3548
|
/**
|
|
2991
3549
|
* Generates SQL from a natural language question and executes it.
|
|
@@ -3884,6 +4442,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
3884
4442
|
QueryErrorCode,
|
|
3885
4443
|
QueryPanelSdkAPI,
|
|
3886
4444
|
QueryPipelineError,
|
|
4445
|
+
SchemaTooLargeError,
|
|
3887
4446
|
anonymizeResults,
|
|
3888
4447
|
mapGeneratedParams,
|
|
3889
4448
|
normalizeSqlParamsInput,
|