@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/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(options.allowedTables);
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.kind = options.kind ?? "postgres";
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 "postgres";
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 clauses = tables.map(({ schema, table }) => {
786
- return `(${schemaExpr} = '${schema}' AND ${tableExpr} = '${table}')`;
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?.error || response.statusText || "Request failed";
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: "postgres",
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,