@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.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)) {
|
|
@@ -501,7 +619,8 @@ var PostgresAdapter = class {
|
|
|
501
619
|
this.clientFn = clientFn;
|
|
502
620
|
this.databaseName = options.database ?? "postgres";
|
|
503
621
|
this.defaultSchema = options.defaultSchema ?? "public";
|
|
504
|
-
this.
|
|
622
|
+
this.dialect = options.dialect ?? "postgres";
|
|
623
|
+
this.kind = options.kind ?? this.dialect;
|
|
505
624
|
if (options.allowedTables) {
|
|
506
625
|
this.allowedTables = normalizeTableFilter3(
|
|
507
626
|
options.allowedTables,
|
|
@@ -513,6 +632,7 @@ var PostgresAdapter = class {
|
|
|
513
632
|
databaseName;
|
|
514
633
|
defaultSchema;
|
|
515
634
|
kind;
|
|
635
|
+
dialect;
|
|
516
636
|
allowedTables;
|
|
517
637
|
async execute(sql, params) {
|
|
518
638
|
if (this.allowedTables) {
|
|
@@ -534,6 +654,9 @@ var PostgresAdapter = class {
|
|
|
534
654
|
const allowedSet = new Set(
|
|
535
655
|
this.allowedTables.map((t) => tableKey2(t.schema, t.table))
|
|
536
656
|
);
|
|
657
|
+
const allowedSchemas = new Set(
|
|
658
|
+
this.allowedTables.filter((t) => t.table === WILDCARD).map((t) => t.schema)
|
|
659
|
+
);
|
|
537
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*/)");
|
|
538
661
|
const tablePattern = /(?:FROM|JOIN)\s+(?:ONLY\s+)?(?:([a-zA-Z_][a-zA-Z0-9_]*)\.)?(["']?[a-zA-Z_][a-zA-Z0-9_]*["']?)/gi;
|
|
539
662
|
const matches = neutralizedSql.matchAll(tablePattern);
|
|
@@ -542,7 +665,7 @@ var PostgresAdapter = class {
|
|
|
542
665
|
const table = match[2]?.replace(/['"]/g, "");
|
|
543
666
|
if (table) {
|
|
544
667
|
const key = tableKey2(schema, table);
|
|
545
|
-
if (!allowedSet.has(key)) {
|
|
668
|
+
if (!allowedSet.has(key) && !allowedSchemas.has(schema)) {
|
|
546
669
|
throw new Error(
|
|
547
670
|
`Query references table "${schema}.${table}" which is not in the allowed tables list`
|
|
548
671
|
);
|
|
@@ -592,7 +715,34 @@ var PostgresAdapter = class {
|
|
|
592
715
|
await this.clientFn(`EXPLAIN ${sql}`, paramArray);
|
|
593
716
|
}
|
|
594
717
|
getDialect() {
|
|
595
|
-
return
|
|
718
|
+
return this.dialect;
|
|
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
|
+
}));
|
|
596
746
|
}
|
|
597
747
|
/**
|
|
598
748
|
* Simplified introspection: only collect table/column metadata for IngestRequest
|
|
@@ -601,12 +751,13 @@ var PostgresAdapter = class {
|
|
|
601
751
|
async introspect(options) {
|
|
602
752
|
const tablesToIntrospect = options?.tables ? normalizeTableFilter3(options.tables, this.defaultSchema) : this.allowedTables;
|
|
603
753
|
const normalizedTables = tablesToIntrospect ?? [];
|
|
754
|
+
const isRedshift = this.dialect === "redshift";
|
|
604
755
|
const tablesResult = await this.clientFn(
|
|
605
|
-
buildTablesQuery2(normalizedTables)
|
|
756
|
+
isRedshift ? buildRedshiftTablesQuery(normalizedTables) : buildTablesQuery2(normalizedTables)
|
|
606
757
|
);
|
|
607
758
|
const tableRows = tablesResult.rows;
|
|
608
759
|
const columnsResult = await this.clientFn(
|
|
609
|
-
buildColumnsQuery2(normalizedTables)
|
|
760
|
+
isRedshift ? buildRedshiftColumnsQuery(normalizedTables) : buildColumnsQuery2(normalizedTables)
|
|
610
761
|
);
|
|
611
762
|
const columnRows = columnsResult.rows;
|
|
612
763
|
const tablesByKey = /* @__PURE__ */ new Map();
|
|
@@ -666,7 +817,7 @@ function normalizeTableFilter3(tables, defaultSchema) {
|
|
|
666
817
|
const parts = trimmed.split(".");
|
|
667
818
|
const table = parts.pop() ?? "";
|
|
668
819
|
const schema = parts.pop() ?? defaultSchema;
|
|
669
|
-
if (!isSafeIdentifier(schema) || !isSafeIdentifier(table)) {
|
|
820
|
+
if (!isSafeIdentifier(schema) || table !== WILDCARD && !isSafeIdentifier(table)) {
|
|
670
821
|
continue;
|
|
671
822
|
}
|
|
672
823
|
const key = tableKey2(schema, table);
|
|
@@ -720,10 +871,11 @@ function buildColumnsQuery2(tables) {
|
|
|
720
871
|
AND kcu.column_name = cols.column_name
|
|
721
872
|
) AS is_primary_key
|
|
722
873
|
FROM information_schema.columns cols
|
|
874
|
+
LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = cols.table_schema
|
|
723
875
|
LEFT JOIN pg_catalog.pg_class c
|
|
724
876
|
ON c.relname = cols.table_name
|
|
877
|
+
AND c.relnamespace = n.oid
|
|
725
878
|
AND c.relkind IN ('r', 'v', 'm')
|
|
726
|
-
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
727
879
|
LEFT JOIN pg_catalog.pg_attribute attr
|
|
728
880
|
ON attr.attrelid = c.oid
|
|
729
881
|
AND attr.attname = cols.column_name
|
|
@@ -733,13 +885,117 @@ function buildColumnsQuery2(tables) {
|
|
|
733
885
|
${filter}
|
|
734
886
|
ORDER BY cols.table_schema, cols.table_name, cols.ordinal_position;`;
|
|
735
887
|
}
|
|
888
|
+
var REDSHIFT_EXCLUDED_SCHEMAS = "('pg_catalog', 'information_schema', 'pg_internal', 'pg_automv')";
|
|
889
|
+
function buildRedshiftTablesQuery(tables) {
|
|
890
|
+
const filter = buildFilterClause(tables, "table_schema", "table_name");
|
|
891
|
+
return `SELECT
|
|
892
|
+
table_name,
|
|
893
|
+
table_schema AS schema_name,
|
|
894
|
+
table_type,
|
|
895
|
+
remarks AS comment
|
|
896
|
+
FROM svv_tables
|
|
897
|
+
WHERE table_catalog = current_database()
|
|
898
|
+
AND table_schema NOT IN ${REDSHIFT_EXCLUDED_SCHEMAS}
|
|
899
|
+
AND table_schema NOT LIKE 'pg_temp%'
|
|
900
|
+
${filter}
|
|
901
|
+
ORDER BY table_schema, table_name;`;
|
|
902
|
+
}
|
|
903
|
+
function buildRedshiftColumnsQuery(tables) {
|
|
904
|
+
const filter = buildFilterClause(tables, "table_schema", "table_name");
|
|
905
|
+
return `SELECT
|
|
906
|
+
table_name,
|
|
907
|
+
table_schema,
|
|
908
|
+
column_name,
|
|
909
|
+
data_type,
|
|
910
|
+
NULL AS udt_name,
|
|
911
|
+
remarks AS description,
|
|
912
|
+
FALSE AS is_primary_key
|
|
913
|
+
FROM svv_columns
|
|
914
|
+
WHERE table_catalog = current_database()
|
|
915
|
+
AND table_schema NOT IN ${REDSHIFT_EXCLUDED_SCHEMAS}
|
|
916
|
+
AND table_schema NOT LIKE 'pg_temp%'
|
|
917
|
+
${filter}
|
|
918
|
+
ORDER BY table_schema, table_name, ordinal_position;`;
|
|
919
|
+
}
|
|
736
920
|
function buildFilterClause(tables, schemaExpr, tableExpr) {
|
|
737
921
|
if (!tables.length) return "";
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
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
|
+
}
|
|
741
933
|
return `AND (${clauses.join(" OR ")})`;
|
|
742
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
|
+
}
|
|
743
999
|
function tableKey2(schema, table) {
|
|
744
1000
|
return `${schema}.${table}`;
|
|
745
1001
|
}
|
|
@@ -874,6 +1130,20 @@ var QueryPipelineError = class extends Error {
|
|
|
874
1130
|
return isAiProviderErrorCode(this.code);
|
|
875
1131
|
}
|
|
876
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
|
+
};
|
|
877
1147
|
|
|
878
1148
|
// src/core/client.ts
|
|
879
1149
|
var ApiClient = class {
|
|
@@ -1013,7 +1283,7 @@ var ApiClient = class {
|
|
|
1013
1283
|
json = void 0;
|
|
1014
1284
|
}
|
|
1015
1285
|
if (!response.ok) {
|
|
1016
|
-
const message = json
|
|
1286
|
+
const message = errorMessageFromBody(json, response);
|
|
1017
1287
|
if (isQueryErrorCode(json?.code)) {
|
|
1018
1288
|
throw new QueryPipelineError(
|
|
1019
1289
|
message,
|
|
@@ -1135,6 +1405,17 @@ var ApiClient = class {
|
|
|
1135
1405
|
return `${data}.${encodedSignature}`;
|
|
1136
1406
|
}
|
|
1137
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
|
+
}
|
|
1138
1419
|
|
|
1139
1420
|
// src/core/callback-client.ts
|
|
1140
1421
|
var CallbackApiClient = class {
|
|
@@ -1921,6 +2202,191 @@ async function syncSchema(client, queryEngine, databaseName, options, signal) {
|
|
|
1921
2202
|
);
|
|
1922
2203
|
return response;
|
|
1923
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
|
+
}
|
|
1924
2390
|
function resolveTenantId4(client, tenantId) {
|
|
1925
2391
|
const resolved = tenantId ?? client.getDefaultTenantId();
|
|
1926
2392
|
if (!resolved) {
|
|
@@ -2866,7 +3332,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2866
3332
|
const adapter = new PostgresAdapter(clientFn, options);
|
|
2867
3333
|
const metadata = {
|
|
2868
3334
|
name,
|
|
2869
|
-
dialect:
|
|
3335
|
+
dialect: adapter.getDialect(),
|
|
2870
3336
|
description: options?.description,
|
|
2871
3337
|
tags: options?.tags,
|
|
2872
3338
|
tenantFieldName: options?.tenantFieldName,
|
|
@@ -2878,6 +3344,14 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2878
3344
|
};
|
|
2879
3345
|
this.queryEngine.attachDatabase(name, adapter, metadata);
|
|
2880
3346
|
}
|
|
3347
|
+
/**
|
|
3348
|
+
* Attach an Amazon Redshift database. Redshift speaks the Postgres wire
|
|
3349
|
+
* protocol, so `clientFn` can wrap a `pg` client; introspection uses
|
|
3350
|
+
* Redshift's svv_* catalog views and SQL is generated for Redshift.
|
|
3351
|
+
*/
|
|
3352
|
+
attachRedshift(name, clientFn, options) {
|
|
3353
|
+
this.attachPostgres(name, clientFn, { ...options, dialect: "redshift" });
|
|
3354
|
+
}
|
|
2881
3355
|
attachBigQuery(name, clientFn, options) {
|
|
2882
3356
|
const adapter = new BigQueryAdapter(clientFn, options);
|
|
2883
3357
|
const metadata = {
|
|
@@ -2906,6 +3380,33 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2906
3380
|
const adapter = this.queryEngine.getDatabase(databaseName);
|
|
2907
3381
|
return await adapter.introspect(tables ? { tables } : void 0);
|
|
2908
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
|
+
}
|
|
2909
3410
|
/**
|
|
2910
3411
|
* Syncs the database schema to QueryPanel for natural language query generation.
|
|
2911
3412
|
*
|
|
@@ -2939,6 +3440,62 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2939
3440
|
signal
|
|
2940
3441
|
);
|
|
2941
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
|
+
}
|
|
2942
3499
|
// Natural language query
|
|
2943
3500
|
/**
|
|
2944
3501
|
* Generates SQL from a natural language question and executes it.
|
|
@@ -3836,6 +4393,7 @@ export {
|
|
|
3836
4393
|
QueryErrorCode,
|
|
3837
4394
|
QueryPanelSdkAPI,
|
|
3838
4395
|
QueryPipelineError,
|
|
4396
|
+
SchemaTooLargeError,
|
|
3839
4397
|
anonymizeResults,
|
|
3840
4398
|
mapGeneratedParams,
|
|
3841
4399
|
normalizeSqlParamsInput,
|