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