@querypanel/node-sdk 1.3.5 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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)) {
@@ -583,6 +702,9 @@ var PostgresAdapter = class {
583
702
  const allowedSet = new Set(
584
703
  this.allowedTables.map((t) => tableKey2(t.schema, t.table))
585
704
  );
705
+ const allowedSchemas = new Set(
706
+ this.allowedTables.filter((t) => t.table === WILDCARD).map((t) => t.schema)
707
+ );
586
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*/)");
587
709
  const tablePattern = /(?:FROM|JOIN)\s+(?:ONLY\s+)?(?:([a-zA-Z_][a-zA-Z0-9_]*)\.)?(["']?[a-zA-Z_][a-zA-Z0-9_]*["']?)/gi;
588
710
  const matches = neutralizedSql.matchAll(tablePattern);
@@ -591,7 +713,7 @@ var PostgresAdapter = class {
591
713
  const table = match[2]?.replace(/['"]/g, "");
592
714
  if (table) {
593
715
  const key = tableKey2(schema, table);
594
- if (!allowedSet.has(key)) {
716
+ if (!allowedSet.has(key) && !allowedSchemas.has(schema)) {
595
717
  throw new Error(
596
718
  `Query references table "${schema}.${table}" which is not in the allowed tables list`
597
719
  );
@@ -643,6 +765,33 @@ var PostgresAdapter = class {
643
765
  getDialect() {
644
766
  return this.dialect;
645
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
+ }));
794
+ }
646
795
  /**
647
796
  * Simplified introspection: only collect table/column metadata for IngestRequest
648
797
  * No indexes, constraints, or statistics
@@ -716,7 +865,7 @@ function normalizeTableFilter3(tables, defaultSchema) {
716
865
  const parts = trimmed.split(".");
717
866
  const table = parts.pop() ?? "";
718
867
  const schema = parts.pop() ?? defaultSchema;
719
- if (!isSafeIdentifier(schema) || !isSafeIdentifier(table)) {
868
+ if (!isSafeIdentifier(schema) || table !== WILDCARD && !isSafeIdentifier(table)) {
720
869
  continue;
721
870
  }
722
871
  const key = tableKey2(schema, table);
@@ -818,11 +967,83 @@ function buildRedshiftColumnsQuery(tables) {
818
967
  }
819
968
  function buildFilterClause(tables, schemaExpr, tableExpr) {
820
969
  if (!tables.length) return "";
821
- const clauses = tables.map(({ schema, table }) => {
822
- return `(${schemaExpr} = '${schema}' AND ${tableExpr} = '${table}')`;
823
- });
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
+ }
824
981
  return `AND (${clauses.join(" OR ")})`;
825
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
+ }
826
1047
  function tableKey2(schema, table) {
827
1048
  return `${schema}.${table}`;
828
1049
  }
@@ -957,6 +1178,20 @@ var QueryPipelineError = class extends Error {
957
1178
  return isAiProviderErrorCode(this.code);
958
1179
  }
959
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
+ };
960
1195
 
961
1196
  // src/core/client.ts
962
1197
  var ApiClient = class {
@@ -1096,7 +1331,7 @@ var ApiClient = class {
1096
1331
  json = void 0;
1097
1332
  }
1098
1333
  if (!response.ok) {
1099
- const message = json?.error || response.statusText || "Request failed";
1334
+ const message = errorMessageFromBody(json, response);
1100
1335
  if (isQueryErrorCode(json?.code)) {
1101
1336
  throw new QueryPipelineError(
1102
1337
  message,
@@ -1218,6 +1453,17 @@ var ApiClient = class {
1218
1453
  return `${data}.${encodedSignature}`;
1219
1454
  }
1220
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
+ }
1221
1467
 
1222
1468
  // src/core/callback-client.ts
1223
1469
  var CallbackApiClient = class {
@@ -2004,6 +2250,191 @@ async function syncSchema(client, queryEngine, databaseName, options, signal) {
2004
2250
  );
2005
2251
  return response;
2006
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
+ }
2007
2438
  function resolveTenantId4(client, tenantId) {
2008
2439
  const resolved = tenantId ?? client.getDefaultTenantId();
2009
2440
  if (!resolved) {
@@ -2997,6 +3428,33 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2997
3428
  const adapter = this.queryEngine.getDatabase(databaseName);
2998
3429
  return await adapter.introspect(tables ? { tables } : void 0);
2999
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
+ }
3000
3458
  /**
3001
3459
  * Syncs the database schema to QueryPanel for natural language query generation.
3002
3460
  *
@@ -3030,6 +3488,62 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
3030
3488
  signal
3031
3489
  );
3032
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
+ }
3033
3547
  // Natural language query
3034
3548
  /**
3035
3549
  * Generates SQL from a natural language question and executes it.
@@ -3928,6 +4442,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
3928
4442
  QueryErrorCode,
3929
4443
  QueryPanelSdkAPI,
3930
4444
  QueryPipelineError,
4445
+ SchemaTooLargeError,
3931
4446
  anonymizeResults,
3932
4447
  mapGeneratedParams,
3933
4448
  normalizeSqlParamsInput,