@querypanel/node-sdk 1.3.5 → 1.3.7

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)) {
@@ -536,6 +654,9 @@ var PostgresAdapter = class {
536
654
  const allowedSet = new Set(
537
655
  this.allowedTables.map((t) => tableKey2(t.schema, t.table))
538
656
  );
657
+ const allowedSchemas = new Set(
658
+ this.allowedTables.filter((t) => t.table === WILDCARD).map((t) => t.schema)
659
+ );
539
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*/)");
540
661
  const tablePattern = /(?:FROM|JOIN)\s+(?:ONLY\s+)?(?:([a-zA-Z_][a-zA-Z0-9_]*)\.)?(["']?[a-zA-Z_][a-zA-Z0-9_]*["']?)/gi;
541
662
  const matches = neutralizedSql.matchAll(tablePattern);
@@ -544,7 +665,7 @@ var PostgresAdapter = class {
544
665
  const table = match[2]?.replace(/['"]/g, "");
545
666
  if (table) {
546
667
  const key = tableKey2(schema, table);
547
- if (!allowedSet.has(key)) {
668
+ if (!allowedSet.has(key) && !allowedSchemas.has(schema)) {
548
669
  throw new Error(
549
670
  `Query references table "${schema}.${table}" which is not in the allowed tables list`
550
671
  );
@@ -596,6 +717,33 @@ var PostgresAdapter = class {
596
717
  getDialect() {
597
718
  return this.dialect;
598
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
+ }));
746
+ }
599
747
  /**
600
748
  * Simplified introspection: only collect table/column metadata for IngestRequest
601
749
  * No indexes, constraints, or statistics
@@ -669,7 +817,7 @@ function normalizeTableFilter3(tables, defaultSchema) {
669
817
  const parts = trimmed.split(".");
670
818
  const table = parts.pop() ?? "";
671
819
  const schema = parts.pop() ?? defaultSchema;
672
- if (!isSafeIdentifier(schema) || !isSafeIdentifier(table)) {
820
+ if (!isSafeIdentifier(schema) || table !== WILDCARD && !isSafeIdentifier(table)) {
673
821
  continue;
674
822
  }
675
823
  const key = tableKey2(schema, table);
@@ -771,11 +919,83 @@ function buildRedshiftColumnsQuery(tables) {
771
919
  }
772
920
  function buildFilterClause(tables, schemaExpr, tableExpr) {
773
921
  if (!tables.length) return "";
774
- const clauses = tables.map(({ schema, table }) => {
775
- return `(${schemaExpr} = '${schema}' AND ${tableExpr} = '${table}')`;
776
- });
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
+ }
777
933
  return `AND (${clauses.join(" OR ")})`;
778
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
+ }
779
999
  function tableKey2(schema, table) {
780
1000
  return `${schema}.${table}`;
781
1001
  }
@@ -910,6 +1130,20 @@ var QueryPipelineError = class extends Error {
910
1130
  return isAiProviderErrorCode(this.code);
911
1131
  }
912
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
+ };
913
1147
 
914
1148
  // src/core/client.ts
915
1149
  var ApiClient = class {
@@ -1049,7 +1283,7 @@ var ApiClient = class {
1049
1283
  json = void 0;
1050
1284
  }
1051
1285
  if (!response.ok) {
1052
- const message = json?.error || response.statusText || "Request failed";
1286
+ const message = errorMessageFromBody(json, response);
1053
1287
  if (isQueryErrorCode(json?.code)) {
1054
1288
  throw new QueryPipelineError(
1055
1289
  message,
@@ -1171,6 +1405,17 @@ var ApiClient = class {
1171
1405
  return `${data}.${encodedSignature}`;
1172
1406
  }
1173
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
+ }
1174
1419
 
1175
1420
  // src/core/callback-client.ts
1176
1421
  var CallbackApiClient = class {
@@ -1957,6 +2202,191 @@ async function syncSchema(client, queryEngine, databaseName, options, signal) {
1957
2202
  );
1958
2203
  return response;
1959
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
+ }
1960
2390
  function resolveTenantId4(client, tenantId) {
1961
2391
  const resolved = tenantId ?? client.getDefaultTenantId();
1962
2392
  if (!resolved) {
@@ -2950,6 +3380,33 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2950
3380
  const adapter = this.queryEngine.getDatabase(databaseName);
2951
3381
  return await adapter.introspect(tables ? { tables } : void 0);
2952
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
+ }
2953
3410
  /**
2954
3411
  * Syncs the database schema to QueryPanel for natural language query generation.
2955
3412
  *
@@ -2983,6 +3440,62 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2983
3440
  signal
2984
3441
  );
2985
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
+ }
2986
3499
  // Natural language query
2987
3500
  /**
2988
3501
  * Generates SQL from a natural language question and executes it.
@@ -3099,7 +3612,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
3099
3612
  * @param input - VizSpec generation input with question, SQL, and result data
3100
3613
  * @param options - Optional settings for tenant and retries
3101
3614
  * @param signal - Optional AbortSignal for cancellation
3102
- * @returns VizSpec specification for chart, table, or metric visualization
3615
+ * @returns VizSpec specification for chart, table, metric, or map visualization
3103
3616
  *
3104
3617
  * @example
3105
3618
  * ```typescript
@@ -3880,6 +4393,7 @@ export {
3880
4393
  QueryErrorCode,
3881
4394
  QueryPanelSdkAPI,
3882
4395
  QueryPipelineError,
4396
+ SchemaTooLargeError,
3883
4397
  anonymizeResults,
3884
4398
  mapGeneratedParams,
3885
4399
  normalizeSqlParamsInput,