@querypanel/node-sdk 1.3.1 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,28 @@
1
1
  # QueryPanel Node SDK
2
2
 
3
+ `@querypanel/node-sdk` is the headless, server-side SDK for [QueryPanel](https://querypanel.io) embedded analytics. Use it in your Node.js (18+), Bun, or Deno backend to mint RS256 tenant JWTs, sync your Postgres, Amazon Redshift, ClickHouse, or BigQuery schema, and turn natural-language questions into SQL (natural-language-to-SQL / text-to-SQL) for multi-tenant SaaS products. The generated SQL runs against your database through a client function you provide, so database credentials and query results stay on your servers. For chart generation the SDK sends column names and value types, not row values (see `anonymizeResults`).
4
+
5
+ - Docs: [querypanel.io/docs/node-sdk](https://querypanel.io/docs/node-sdk)
6
+ - Tenant isolation: [querypanel.io/docs/tenant-isolation](https://querypanel.io/docs/tenant-isolation)
7
+ - JWT setup: [querypanel.io/docs/jwt](https://querypanel.io/docs/jwt)
8
+
9
+ ## When to use the Node SDK vs the React SDK
10
+
11
+ - **[React SDK](https://www.npmjs.com/package/@querypanel/react-sdk) (headful, start here for React apps):** `@querypanel/react-sdk` renders customer-facing dashboards with an AI assistant and per-tenant customization directly in your React tree, not an iframe ([why that matters](https://querypanel.io/blog/iframe-vs-native-react-embedded-analytics-2026)). It still needs this package on your server to mint the tenant JWT (`createJwt`).
12
+ - **Node SDK (this package, headless):** use it alone when you build your own UI and want `ask()` to return SQL, rows, and a chart spec from your API routes.
13
+
14
+ How it compares to other embedded analytics tools: [querypanel.io/compare](https://querypanel.io/compare).
15
+
16
+ ## Overview
17
+
3
18
  A TypeScript-first client for the QueryPanel Bun/Hono API. Its primary function is to **generate SQL from natural language**, but it also signs JWTs with your service private key, syncs database schemas, enforces tenant isolation, and wraps every public route under `src/routes/` (query, ingest, charts, active charts, and knowledge base).
4
19
 
5
20
  ## Installation
6
21
 
7
22
  ```bash
8
- bun add @querypanel/sdk
23
+ bun add @querypanel/node-sdk
9
24
  # or
10
- npm install @querypanel/sdk
25
+ npm install @querypanel/node-sdk
11
26
  ```
12
27
 
13
28
  > **Runtime:** Node.js 18+, Deno, or Bun. The SDK uses Web Crypto API for JWT signing and the native `fetch` API, making it compatible with modern JavaScript runtimes.
@@ -15,8 +30,9 @@ npm install @querypanel/sdk
15
30
  ## Quickstart
16
31
 
17
32
  ```ts
18
- import { QueryPanelSdkAPI } from "@querypanel/sdk";
33
+ import { QueryPanelSdkAPI } from "@querypanel/node-sdk";
19
34
  import { Pool } from "pg";
35
+ import { createClient } from "@clickhouse/client";
20
36
 
21
37
  const qp = new QueryPanelSdkAPI(
22
38
  process.env.QUERYPANEL_URL!,
@@ -42,21 +58,31 @@ const createPostgresClient = () => async (sql: string, params?: unknown[]) => {
42
58
  }
43
59
  };
44
60
 
45
- // Attach PostgreSQL database using the SDK's PostgresAdapter
46
- // The SDK will automatically handle tenant isolation when tenantFieldName is provided
47
- qp.attachPostgres(
48
- "pg_demo", // a uniq identifier for QueryPanel
49
- createPostgresClientFn(),
61
+ // Attach PostgreSQL database using the SDK's PostgresAdapter
62
+ // The SDK will automatically handle tenant isolation when tenantFieldName is provided
63
+ qp.attachPostgres(
64
+ "pg_demo", // a unique identifier for QueryPanel
65
+ createPostgresClient(),
50
66
  {
51
- database: "pg_demo", // database name
52
- description: "PostgreSQL demo database", // some description that QueryPanel can use
53
- tenantFieldName: "tenant_id", // SDK will automatically filter by tenant_id
54
- enforceTenantIsolation: true, // Ensures all queries include tenant_id filter
55
- allowedTables: ["orders"], // Only sync 'orders' table - 'users' will be excluded
56
- });
67
+ database: "pg_demo", // database name
68
+ description: "PostgreSQL demo database", // some description that QueryPanel can use
69
+ tenantFieldName: "tenant_id", // SDK will automatically filter by tenant_id
70
+ enforceTenantIsolation: true, // Ensures all queries include tenant_id filter
71
+ allowedTables: ["orders"], // Only sync 'orders' table - 'users' will be excluded
72
+ },
73
+ );
74
+
75
+ // Amazon Redshift speaks the Postgres wire protocol, so a pg-based client works.
76
+ // attachRedshift introspects via Redshift's svv_* views and generates Redshift SQL.
77
+ qp.attachRedshift("warehouse", createPostgresClient(), {
78
+ database: "dev",
79
+ tenantFieldName: "tenant_id",
80
+ });
81
+
82
+ const clickhouse = createClient({ url: process.env.CLICKHOUSE_URL });
57
83
 
58
84
  qp.attachClickhouse(
59
- "analytics", // uniq identifier for QueryPanel
85
+ "analytics", // unique identifier for QueryPanel
60
86
  (params) => clickhouse.query(params),
61
87
  {
62
88
  database: "analytics", // database name
@@ -143,7 +169,7 @@ const response = await qp.ask("Show order volume by week", {
143
169
  If your UI only supports a subset of chart kinds (for example no area or scatter), set **`supportedChartTypes`** on the SDK constructor or on each `ask` / `modifyChart` call (`line`, `bar`, `column`, `area`, `scatter`, `pie`).
144
170
 
145
171
  ```ts
146
- import { QueryPanelSdkAPI, ALL_VIZ_CHART_TYPES, type ChartType } from "@querypanel/sdk";
172
+ import { QueryPanelSdkAPI, ALL_VIZ_CHART_TYPES, type ChartType } from "@querypanel/node-sdk";
147
173
 
148
174
  const allowed: ChartType[] = ["line", "bar", "column", "pie"];
149
175
 
@@ -512,7 +538,7 @@ The API accepts up to **100** UUIDs per request. Examples: `?ids=a&ids=b` or `?i
512
538
  The SDK is fully compatible with Deno (including Supabase Edge Functions) thanks to its use of Web Crypto API for JWT signing. No additional configuration needed:
513
539
 
514
540
  ```ts
515
- import { QueryPanelSdkAPI } from "https://esm.sh/@querypanel/sdk";
541
+ import { QueryPanelSdkAPI } from "https://esm.sh/@querypanel/node-sdk";
516
542
 
517
543
  const qp = new QueryPanelSdkAPI(
518
544
  Deno.env.get("QUERYPANEL_URL")!,
@@ -526,10 +552,12 @@ const response = await qp.ask("Show top products", {
526
552
  });
527
553
  ```
528
554
 
555
+ BigQuery is attached the same way with `attachBigQuery(name, clientFn, options)`. Any other engine can be plugged in with `attachDatabase(name, adapter)` and your own `DatabaseAdapter` implementation.
556
+
529
557
  ## Building locally
530
558
 
531
559
  ```bash
532
- cd node-sdk
560
+ cd admin/packages/node-sdk
533
561
  bun install
534
562
  bun run build
535
563
  ```
@@ -571,4 +599,4 @@ Without `maxRetry`, execution errors throw immediately (default behavior).
571
599
 
572
600
  ## Need more?
573
601
 
574
- Open an issue or extend `node-sdk/src/index.ts`—every route lives in one file. Pull requests are welcome for additional adapters, richer param coercion, or convenience helpers around charts/annotations.
602
+ Open an issue or extend the SDK: the public class is in `src/index.ts` and each API route has a module under `src/routes/`. Pull requests are welcome for additional adapters, richer param coercion, or convenience helpers around charts/annotations.
package/dist/index.cjs CHANGED
@@ -548,7 +548,8 @@ var PostgresAdapter = class {
548
548
  this.clientFn = clientFn;
549
549
  this.databaseName = options.database ?? "postgres";
550
550
  this.defaultSchema = options.defaultSchema ?? "public";
551
- this.kind = options.kind ?? "postgres";
551
+ this.dialect = options.dialect ?? "postgres";
552
+ this.kind = options.kind ?? this.dialect;
552
553
  if (options.allowedTables) {
553
554
  this.allowedTables = normalizeTableFilter3(
554
555
  options.allowedTables,
@@ -560,6 +561,7 @@ var PostgresAdapter = class {
560
561
  databaseName;
561
562
  defaultSchema;
562
563
  kind;
564
+ dialect;
563
565
  allowedTables;
564
566
  async execute(sql, params) {
565
567
  if (this.allowedTables) {
@@ -639,7 +641,7 @@ var PostgresAdapter = class {
639
641
  await this.clientFn(`EXPLAIN ${sql}`, paramArray);
640
642
  }
641
643
  getDialect() {
642
- return "postgres";
644
+ return this.dialect;
643
645
  }
644
646
  /**
645
647
  * Simplified introspection: only collect table/column metadata for IngestRequest
@@ -648,12 +650,13 @@ var PostgresAdapter = class {
648
650
  async introspect(options) {
649
651
  const tablesToIntrospect = options?.tables ? normalizeTableFilter3(options.tables, this.defaultSchema) : this.allowedTables;
650
652
  const normalizedTables = tablesToIntrospect ?? [];
653
+ const isRedshift = this.dialect === "redshift";
651
654
  const tablesResult = await this.clientFn(
652
- buildTablesQuery2(normalizedTables)
655
+ isRedshift ? buildRedshiftTablesQuery(normalizedTables) : buildTablesQuery2(normalizedTables)
653
656
  );
654
657
  const tableRows = tablesResult.rows;
655
658
  const columnsResult = await this.clientFn(
656
- buildColumnsQuery2(normalizedTables)
659
+ isRedshift ? buildRedshiftColumnsQuery(normalizedTables) : buildColumnsQuery2(normalizedTables)
657
660
  );
658
661
  const columnRows = columnsResult.rows;
659
662
  const tablesByKey = /* @__PURE__ */ new Map();
@@ -767,10 +770,11 @@ function buildColumnsQuery2(tables) {
767
770
  AND kcu.column_name = cols.column_name
768
771
  ) AS is_primary_key
769
772
  FROM information_schema.columns cols
773
+ LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = cols.table_schema
770
774
  LEFT JOIN pg_catalog.pg_class c
771
775
  ON c.relname = cols.table_name
776
+ AND c.relnamespace = n.oid
772
777
  AND c.relkind IN ('r', 'v', 'm')
773
- LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
774
778
  LEFT JOIN pg_catalog.pg_attribute attr
775
779
  ON attr.attrelid = c.oid
776
780
  AND attr.attname = cols.column_name
@@ -780,6 +784,38 @@ function buildColumnsQuery2(tables) {
780
784
  ${filter}
781
785
  ORDER BY cols.table_schema, cols.table_name, cols.ordinal_position;`;
782
786
  }
787
+ var REDSHIFT_EXCLUDED_SCHEMAS = "('pg_catalog', 'information_schema', 'pg_internal', 'pg_automv')";
788
+ function buildRedshiftTablesQuery(tables) {
789
+ const filter = buildFilterClause(tables, "table_schema", "table_name");
790
+ return `SELECT
791
+ table_name,
792
+ table_schema AS schema_name,
793
+ table_type,
794
+ remarks AS comment
795
+ FROM svv_tables
796
+ WHERE table_catalog = current_database()
797
+ AND table_schema NOT IN ${REDSHIFT_EXCLUDED_SCHEMAS}
798
+ AND table_schema NOT LIKE 'pg_temp%'
799
+ ${filter}
800
+ ORDER BY table_schema, table_name;`;
801
+ }
802
+ function buildRedshiftColumnsQuery(tables) {
803
+ const filter = buildFilterClause(tables, "table_schema", "table_name");
804
+ return `SELECT
805
+ table_name,
806
+ table_schema,
807
+ column_name,
808
+ data_type,
809
+ NULL AS udt_name,
810
+ remarks AS description,
811
+ FALSE AS is_primary_key
812
+ FROM svv_columns
813
+ WHERE table_catalog = current_database()
814
+ AND table_schema NOT IN ${REDSHIFT_EXCLUDED_SCHEMAS}
815
+ AND table_schema NOT LIKE 'pg_temp%'
816
+ ${filter}
817
+ ORDER BY table_schema, table_name, ordinal_position;`;
818
+ }
783
819
  function buildFilterClause(tables, schemaExpr, tableExpr) {
784
820
  if (!tables.length) return "";
785
821
  const clauses = tables.map(({ schema, table }) => {
@@ -2292,6 +2328,7 @@ function buildModifiedQuestion(originalQuestion, modifications, hasSessionContex
2292
2328
  }
2293
2329
  return `${originalQuestion} (${hints.join(", ")})`;
2294
2330
  }
2331
+ var REWRITE_DATEFILTER_ENDPOINT = "/v2/rewrite-datefilter";
2295
2332
  var START_PARAM_KEY_REGEX = /(^|_)(start|from)(_|$)/i;
2296
2333
  var END_PARAM_KEY_REGEX = /(^|_)(end|to)(_|$)/i;
2297
2334
  var ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?$/;
@@ -2328,31 +2365,118 @@ function normalizeGeneratedParamKey(param, index) {
2328
2365
  const nameCandidate = typeof param.name === "string" && param.name.trim() || typeof param.placeholder === "string" && param.placeholder.trim() || typeof param.position === "number" && String(param.position) || String(index + 1);
2329
2366
  return nameCandidate.replace(/[{}]/g, "").replace(/(.+):.*$/, "$1").replace(/^[:$]/, "").trim();
2330
2367
  }
2331
- function applyDateRangeOverrides(dateRange, params, paramMetadata) {
2368
+ function collectParamKeys(params, paramMetadata) {
2369
+ const keys = new Set(Object.keys(params));
2370
+ for (let i = 0; i < paramMetadata.length; i++) {
2371
+ const param = paramMetadata[i];
2372
+ if (!param) continue;
2373
+ keys.add(normalizeGeneratedParamKey(param, i));
2374
+ }
2375
+ return [...keys];
2376
+ }
2377
+ function placeholderKey(token) {
2378
+ if (token.startsWith("$") || token.startsWith("@")) return token.slice(1);
2379
+ return token.slice(1, -1).replace(/:.+$/, "").trim();
2380
+ }
2381
+ function isDateLike(value) {
2382
+ return typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value.trim());
2383
+ }
2384
+ function dateBoundKeysFromSql(sql) {
2385
+ const start = /* @__PURE__ */ new Set();
2386
+ const end = /* @__PURE__ */ new Set();
2387
+ const comparison = /(>=|<=|>|<)\s*(\$\d+|\{[^}]+\}|@[A-Za-z_][\w]*)/g;
2388
+ const between = /BETWEEN\s+(\$\d+|\{[^}]+\}|@[A-Za-z_][\w]*)\s+AND\s+(\$\d+|\{[^}]+\}|@[A-Za-z_][\w]*)/gi;
2389
+ for (const match of sql.matchAll(comparison)) {
2390
+ const key = placeholderKey(match[2] ?? "");
2391
+ if (!key) continue;
2392
+ if (match[1] === ">=" || match[1] === ">") start.add(key);
2393
+ else end.add(key);
2394
+ }
2395
+ for (const match of sql.matchAll(between)) {
2396
+ const startKey = placeholderKey(match[1] ?? "");
2397
+ const endKey = placeholderKey(match[2] ?? "");
2398
+ if (startKey) start.add(startKey);
2399
+ if (endKey) end.add(endKey);
2400
+ }
2401
+ return { start, end };
2402
+ }
2403
+ function paramValueForKey(key, params, paramMetadata) {
2404
+ if (key in params) return params[key];
2405
+ for (let i = 0; i < paramMetadata.length; i++) {
2406
+ const param = paramMetadata[i];
2407
+ if (!param) continue;
2408
+ if (normalizeGeneratedParamKey(param, i) === key) return param.value;
2409
+ }
2410
+ return void 0;
2411
+ }
2412
+ function dateBoundKeys(sql, params, paramMetadata, boundary) {
2413
+ const fromSql = dateBoundKeysFromSql(sql)[boundary];
2414
+ const keys = [];
2415
+ for (const key of collectParamKeys(params, paramMetadata)) {
2416
+ const named = keyLooksLikeDateBoundary(key, boundary);
2417
+ if (!named && !fromSql.has(key)) continue;
2418
+ if (!isDateLike(paramValueForKey(key, params, paramMetadata))) continue;
2419
+ keys.push(key);
2420
+ }
2421
+ return keys;
2422
+ }
2423
+ function canSubstituteDateRangeParams(dateRange, sql, params, paramMetadata = []) {
2424
+ if (!dateRange) return false;
2425
+ const from = normalizeDateInput(dateRange.from);
2426
+ const to = normalizeDateInput(dateRange.to);
2427
+ if (!from && !to) return false;
2428
+ if (from && dateBoundKeys(sql, params, paramMetadata, "start").length === 0) {
2429
+ return false;
2430
+ }
2431
+ if (to && dateBoundKeys(sql, params, paramMetadata, "end").length === 0) {
2432
+ return false;
2433
+ }
2434
+ return true;
2435
+ }
2436
+ function isDateRangeOnlySqlModification(modifications) {
2437
+ if (!modifications?.dateRange) return false;
2438
+ if (modifications.customSql) return false;
2439
+ if (modifications.timeGranularity) return false;
2440
+ if (typeof modifications.additionalInstructions === "string" && modifications.additionalInstructions.trim().length > 0) {
2441
+ return false;
2442
+ }
2443
+ return true;
2444
+ }
2445
+ function applyDateRangeOverrides(dateRange, sql, params, paramMetadata) {
2332
2446
  if (!dateRange) return;
2333
2447
  const from = normalizeDateInput(dateRange.from);
2334
2448
  const to = normalizeDateInput(dateRange.to);
2335
2449
  if (!from && !to) return;
2450
+ const startKeys = new Set(
2451
+ from ? dateBoundKeys(sql, params, paramMetadata, "start") : []
2452
+ );
2453
+ const endKeys = new Set(
2454
+ to ? dateBoundKeys(sql, params, paramMetadata, "end") : []
2455
+ );
2336
2456
  for (const [key, value] of Object.entries(params)) {
2337
- if (from && keyLooksLikeDateBoundary(key, "start")) {
2338
- params[key] = formatDateOverride(from, "start", value);
2457
+ if (startKeys.has(key)) {
2458
+ params[key] = formatDateOverride(from ?? "", "start", value);
2339
2459
  }
2340
- if (to && keyLooksLikeDateBoundary(key, "end")) {
2341
- params[key] = formatDateOverride(to, "end", value);
2460
+ if (endKeys.has(key)) {
2461
+ params[key] = formatDateOverride(to ?? "", "end", value);
2342
2462
  }
2343
2463
  }
2344
2464
  for (let i = 0; i < paramMetadata.length; i++) {
2345
2465
  const param = paramMetadata[i];
2346
2466
  if (!param) continue;
2347
2467
  const key = normalizeGeneratedParamKey(param, i);
2348
- if (from && keyLooksLikeDateBoundary(key, "start")) {
2349
- param.value = formatDateOverride(from, "start", param.value);
2468
+ if (startKeys.has(key)) {
2469
+ param.value = formatDateOverride(from ?? "", "start", param.value);
2350
2470
  }
2351
- if (to && keyLooksLikeDateBoundary(key, "end")) {
2352
- param.value = formatDateOverride(to, "end", param.value);
2471
+ if (endKeys.has(key)) {
2472
+ param.value = formatDateOverride(to ?? "", "end", param.value);
2353
2473
  }
2354
2474
  }
2355
2475
  }
2476
+ function buildPreviousParams(params, paramMetadata) {
2477
+ if (paramMetadata.length > 0) return paramMetadata;
2478
+ return Object.entries(params).map(([name, value]) => ({ name, value }));
2479
+ }
2356
2480
  function buildVizHints(modifications) {
2357
2481
  const hints = {};
2358
2482
  if (modifications.kind) {
@@ -2403,6 +2527,9 @@ async function modifyChart(client, queryEngine, input, options, signal) {
2403
2527
  const hasSqlMods = !!input.sqlModifications;
2404
2528
  const hasVizMods = !!input.vizModifications;
2405
2529
  const hasCustomSql = !!input.sqlModifications?.customSql;
2530
+ const lightweight = options?.lightweight === true;
2531
+ const dateOnly = lightweight && isDateRangeOnlySqlModification(input.sqlModifications);
2532
+ const skipChartLlm = lightweight && !hasCustomSql && (dateOnly || hasVizMods && !hasSqlMods);
2406
2533
  resolveQueryPipeline(options?.pipeline);
2407
2534
  const queryEndpoint = "/v2/query";
2408
2535
  let currentSql = input.sql;
@@ -2456,6 +2583,20 @@ async function modifyChart(client, queryEngine, input, options, signal) {
2456
2583
  paramMetadata = [];
2457
2584
  queryId = void 0;
2458
2585
  sqlChanged = true;
2586
+ } else if (dateOnly && canSubstituteDateRangeParams(
2587
+ input.sqlModifications?.dateRange,
2588
+ finalSql,
2589
+ finalParams,
2590
+ paramMetadata
2591
+ )) {
2592
+ finalParams = { ...finalParams };
2593
+ applyDateRangeOverrides(
2594
+ input.sqlModifications?.dateRange,
2595
+ finalSql,
2596
+ finalParams,
2597
+ paramMetadata
2598
+ );
2599
+ sqlChanged = false;
2459
2600
  } else if (hasSqlMods && !hasCustomSql) {
2460
2601
  const modifiedQuestion = buildModifiedQuestion(
2461
2602
  input.question,
@@ -2463,18 +2604,36 @@ async function modifyChart(client, queryEngine, input, options, signal) {
2463
2604
  hasQuerypanelSessionContext
2464
2605
  );
2465
2606
  finalQuestion = modifiedQuestion;
2466
- const queryResponse = await client.post(
2607
+ const sharedBody = {
2608
+ session_id: querypanelSessionId,
2609
+ previous_sql: currentSql,
2610
+ ...tenantSettings ? { tenant_settings: tenantSettings } : {},
2611
+ ...databaseName ? { database: databaseName } : {},
2612
+ ...metadata?.dialect ? { dialect: metadata.dialect } : {},
2613
+ debug: options?.debug ?? false
2614
+ };
2615
+ const [endpoint, body] = dateOnly ? [
2616
+ REWRITE_DATEFILTER_ENDPOINT,
2617
+ {
2618
+ ...sharedBody,
2619
+ question: input.question,
2620
+ previous_params: buildPreviousParams(finalParams, paramMetadata),
2621
+ date_range: {
2622
+ from: normalizeDateInput(input.sqlModifications?.dateRange?.from),
2623
+ to: normalizeDateInput(input.sqlModifications?.dateRange?.to)
2624
+ }
2625
+ }
2626
+ ] : [
2467
2627
  queryEndpoint,
2468
2628
  {
2629
+ ...sharedBody,
2469
2630
  question: modifiedQuestion,
2470
- session_id: querypanelSessionId,
2471
- previous_sql: currentSql,
2472
- ...options?.maxRetry ? { max_retry: options.maxRetry } : {},
2473
- ...tenantSettings ? { tenant_settings: tenantSettings } : {},
2474
- ...databaseName ? { database: databaseName } : {},
2475
- ...metadata?.dialect ? { dialect: metadata.dialect } : {},
2476
- debug: options?.debug ?? false
2477
- },
2631
+ ...options?.maxRetry ? { max_retry: options.maxRetry } : {}
2632
+ }
2633
+ ];
2634
+ const queryResponse = await client.post(
2635
+ endpoint,
2636
+ body,
2478
2637
  tenantId,
2479
2638
  options?.userId,
2480
2639
  options?.scopes,
@@ -2486,6 +2645,7 @@ async function modifyChart(client, queryEngine, input, options, signal) {
2486
2645
  finalParams = queryEngine.mapGeneratedParams(paramMetadata);
2487
2646
  applyDateRangeOverrides(
2488
2647
  input.sqlModifications?.dateRange,
2648
+ finalSql,
2489
2649
  finalParams,
2490
2650
  paramMetadata
2491
2651
  );
@@ -2506,7 +2666,7 @@ async function modifyChart(client, queryEngine, input, options, signal) {
2506
2666
  };
2507
2667
  const vizHints = hasVizMods ? buildVizHints(input.vizModifications) : {};
2508
2668
  const vizHintsForChart = chartType === "vizspec" ? vizHints : stripVizSpecOnlyHints(vizHints);
2509
- if (chartType === "vizspec") {
2669
+ if (!skipChartLlm && chartType === "vizspec") {
2510
2670
  const vizspecResponse = await client.post(
2511
2671
  "/vizspec",
2512
2672
  {
@@ -2533,7 +2693,7 @@ async function modifyChart(client, queryEngine, input, options, signal) {
2533
2693
  specType: "vizspec",
2534
2694
  notes: rows.length === 0 ? vizspecResponse.notes ?? "Query returned no rows." : vizspecResponse.notes
2535
2695
  };
2536
- } else {
2696
+ } else if (!skipChartLlm) {
2537
2697
  const chartResponse = await client.post(
2538
2698
  "/chart",
2539
2699
  {
@@ -2789,7 +2949,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2789
2949
  const adapter = new PostgresAdapter(clientFn, options);
2790
2950
  const metadata = {
2791
2951
  name,
2792
- dialect: "postgres",
2952
+ dialect: adapter.getDialect(),
2793
2953
  description: options?.description,
2794
2954
  tags: options?.tags,
2795
2955
  tenantFieldName: options?.tenantFieldName,
@@ -2801,6 +2961,14 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2801
2961
  };
2802
2962
  this.queryEngine.attachDatabase(name, adapter, metadata);
2803
2963
  }
2964
+ /**
2965
+ * Attach an Amazon Redshift database. Redshift speaks the Postgres wire
2966
+ * protocol, so `clientFn` can wrap a `pg` client; introspection uses
2967
+ * Redshift's svv_* catalog views and SQL is generated for Redshift.
2968
+ */
2969
+ attachRedshift(name, clientFn, options) {
2970
+ this.attachPostgres(name, clientFn, { ...options, dialect: "redshift" });
2971
+ }
2804
2972
  attachBigQuery(name, clientFn, options) {
2805
2973
  const adapter = new BigQueryAdapter(clientFn, options);
2806
2974
  const metadata = {