@querypanel/node-sdk 1.3.7 → 1.3.9

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.d.cts CHANGED
@@ -230,6 +230,29 @@ declare class ClickHouseAdapter implements DatabaseAdapter {
230
230
  private extractRows;
231
231
  }
232
232
 
233
+ /** Attach option for schema-per-tenant isolation. */
234
+ interface TenantSchemaConfig {
235
+ /** Schema name with one `{tenant}` placeholder, e.g. `group_{tenant}_indexed`. */
236
+ template: string;
237
+ /** Extra schemas every tenant may read (lookup tables etc.). */
238
+ sharedSchemas?: string[];
239
+ }
240
+ declare class TenantSchemaError extends Error {
241
+ constructor(message: string);
242
+ }
243
+ interface ParsedTemplate {
244
+ prefix: string;
245
+ suffix: string;
246
+ }
247
+ declare function parseTenantSchemaTemplate(template: string): ParsedTemplate;
248
+ /** The schema a tenant's queries run against. Throws for unsafe tenant ids. */
249
+ declare function resolveTenantSchema(template: string, tenantId: string): string;
250
+ /**
251
+ * Rewrite every template-schema qualifier to the tenant's schema. Fails closed when
252
+ * another tenant's schema is still named anywhere (strings and comments included).
253
+ */
254
+ declare function rewriteTenantSchema(sql: string, template: string, tenantId: string): string;
255
+
233
256
  interface PostgresQueryResult {
234
257
  rows: Array<Record<string, unknown>>;
235
258
  fields: Array<{
@@ -257,6 +280,12 @@ interface PostgresAdapterOptions {
257
280
  * to these tables only.
258
281
  */
259
282
  allowedTables?: string[];
283
+ /**
284
+ * Schema-per-tenant connection: SQL has been rewritten to the caller's tenant
285
+ * schema, so queries may read any schema the template produces (or the tables
286
+ * `allowedTables` names in it) plus `sharedSchemas`, and nothing else.
287
+ */
288
+ tenantSchema?: TenantSchemaConfig;
260
289
  }
261
290
  /**
262
291
  * Simplified PostgreSQL adapter following IngestRequest format
@@ -270,6 +299,8 @@ declare class PostgresAdapter implements DatabaseAdapter {
270
299
  private readonly kind;
271
300
  private readonly dialect;
272
301
  private readonly allowedTables?;
302
+ /** Undefined when queries are unrestricted. */
303
+ private readonly allowsRelation?;
273
304
  constructor(clientFn: PostgresClientFn, options?: PostgresAdapterOptions);
274
305
  execute(sql: string, params?: Record<string, string | number | boolean | string[] | number[]>): Promise<DatabaseExecutionResult>;
275
306
  private validateQueryTables;
@@ -867,6 +898,12 @@ interface AskOptions {
867
898
  previousSql?: string;
868
899
  /** Maximum number of retry attempts on execution failure. */
869
900
  maxRetry?: number;
901
+ /**
902
+ * Also retry (within `maxRetry`) when the query returns no rows, or one row of only zeros/NULLs
903
+ * (what `COUNT(*) ... WHERE status = 'approved'` returns when the data stores 'APPROVE').
904
+ * Only that fact is sent to QueryPanel, never rows or values. Default false.
905
+ */
906
+ retryOnEmptyResult?: boolean;
870
907
  /** Maximum number of retries for chart generation. */
871
908
  chartMaxRetries?: number;
872
909
  /** Choose chart generation method. */
@@ -1321,6 +1358,16 @@ declare function mapGeneratedParams(params: Array<Record<string, unknown>>): Par
1321
1358
  */
1322
1359
  declare function normalizeSqlParamsInput(params: unknown[] | Record<string, unknown> | null | undefined): ParamRecord;
1323
1360
 
1361
+ /** Schema-per-tenant option for attachPostgres / attachRedshift (instead of tenantFieldName). */
1362
+ interface TenantSchemaAttachOptions {
1363
+ /**
1364
+ * Each tenant has its own schema, e.g. `{ template: "group_{tenant}_indexed" }`.
1365
+ * SQL naming any schema the template produces runs against the caller's tenant
1366
+ * schema, and only that schema (plus `sharedSchemas`) may be read.
1367
+ */
1368
+ tenantSchema?: TenantSchemaConfig;
1369
+ }
1370
+
1324
1371
  /** Options when constructing the SDK with HTTP (default) or with a custom API implementation. */
1325
1372
  interface QueryPanelSdkAPIOptions {
1326
1373
  defaultTenantId?: string;
@@ -1400,7 +1447,7 @@ declare class QueryPanelSdkAPI {
1400
1447
  tenantFieldName?: string;
1401
1448
  tenantFieldType?: string;
1402
1449
  enforceTenantIsolation?: boolean;
1403
- }): void;
1450
+ } & TenantSchemaAttachOptions): void;
1404
1451
  /**
1405
1452
  * Attach an Amazon Redshift database. Redshift speaks the Postgres wire
1406
1453
  * protocol, so `clientFn` can wrap a `pg` client; introspection uses
@@ -1412,7 +1459,7 @@ declare class QueryPanelSdkAPI {
1412
1459
  tenantFieldName?: string;
1413
1460
  tenantFieldType?: string;
1414
1461
  enforceTenantIsolation?: boolean;
1415
- }): void;
1462
+ } & TenantSchemaAttachOptions): void;
1416
1463
  attachBigQuery(name: string, clientFn: BigQueryClientFn, options: BigQueryAdapterOptions & {
1417
1464
  description?: string;
1418
1465
  tags?: string[];
@@ -2187,4 +2234,4 @@ declare class QueryPanelSdkAPI {
2187
2234
  }, signal?: AbortSignal): Promise<SdkDashboard[]>;
2188
2235
  }
2189
2236
 
2190
- export { ALL_VIZ_CHART_TYPES, type ActiveChartBulkGetResponse, type ActiveChartCreateInput, type ActiveChartListOptions, type ActiveChartUpdateInput, type AggregateOp, type AskOptions, type AskResponse, type AxisField, type AxisFieldInput, BigQueryAdapter, type BigQueryAdapterOptions, type BigQueryClientFn, type BigQueryQueryRequest, type BigQueryQueryResult, CallbackApiClient, type CatalogSchema, type CatalogTable, type CatalogTableType, type ChartBulkGetResponse, type ChartCreateInput, type ChartEncoding, type ChartEnvelope, type ChartListAllOptions, type ChartListOptions, type ChartModifyInput, type ChartModifyOptions, type ChartModifyResponse, type ChartSpec, type ChartType, type ChartUpdateInput, ClickHouseAdapter, type ClickHouseAdapterOptions, type ClickHouseClientFn, type ContextDocument, type DashboardCreateInput, type DashboardForkInput, type DashboardForkUpdateInput, type DashboardListOptions, type DashboardUpdateInput, type DatabaseAdapter, type DatabaseDialect, type DateRangeInput, type EncodingHints, type FieldRef, type FieldRefInput, type FieldType, type IQueryPanelApi, type IngestResponse, type IntentResult, type ListTablesOptions, type MapEncoding, type MapLocation, type MapLocationMode, type MapSpec, type MetricEncoding, type MetricField, type MetricSpec, type PaginatedResponse$1 as PaginatedResponse, type PaginationInfo$1 as PaginationInfo, type PaginationQuery$1 as PaginationQuery, type ParamRecord, type ParamValue, type PipelineTrace, PostgresAdapter, type PostgresAdapterOptions, type PostgresClientFn, type PostgresWireDialect, QueryErrorCode, QueryErrorCode as QueryErrorCodeType, QueryPanelSdkAPI, type QueryPanelSdkAPIOptions, QueryPipelineError, type RequestHandler, type RequestHandlerOptions, type RequestHandlerResult, type SchemaIngestBatchEmbedResponse, type SchemaIngestRequestOptions, type SchemaIntrospection, type SchemaSyncOptions, type SchemaSyncProgressEvent, SchemaTooLargeError, type SdkActiveChart, type SdkChart, type SdkDashboard, type SdkSession, type SdkSessionTurn, type SessionGetOptions, type SessionListOptions, type SessionUpdateInput, type SqlModifications, type StackingMode, type StageSchemaIngestOptions, type StageSchemaIngestResult, type SyncSchemaInBatchesOptions, type TableColumn, type TableEncoding, type TableSpec, type TimeUnit, type ValueFormat, type VizModifications, type VizSpec, type VizSpecGenerateInput, type VizSpecGenerateOptions, type VizSpecGeneratorInput, type VizSpecKind, type VizSpecResponse, type VizSpecResult, anonymizeResults, mapGeneratedParams, normalizeSqlParamsInput, resolveQueryPipeline };
2237
+ export { ALL_VIZ_CHART_TYPES, type ActiveChartBulkGetResponse, type ActiveChartCreateInput, type ActiveChartListOptions, type ActiveChartUpdateInput, type AggregateOp, type AskOptions, type AskResponse, type AxisField, type AxisFieldInput, BigQueryAdapter, type BigQueryAdapterOptions, type BigQueryClientFn, type BigQueryQueryRequest, type BigQueryQueryResult, CallbackApiClient, type CatalogSchema, type CatalogTable, type CatalogTableType, type ChartBulkGetResponse, type ChartCreateInput, type ChartEncoding, type ChartEnvelope, type ChartListAllOptions, type ChartListOptions, type ChartModifyInput, type ChartModifyOptions, type ChartModifyResponse, type ChartSpec, type ChartType, type ChartUpdateInput, ClickHouseAdapter, type ClickHouseAdapterOptions, type ClickHouseClientFn, type ContextDocument, type DashboardCreateInput, type DashboardForkInput, type DashboardForkUpdateInput, type DashboardListOptions, type DashboardUpdateInput, type DatabaseAdapter, type DatabaseDialect, type DateRangeInput, type EncodingHints, type FieldRef, type FieldRefInput, type FieldType, type IQueryPanelApi, type IngestResponse, type IntentResult, type ListTablesOptions, type MapEncoding, type MapLocation, type MapLocationMode, type MapSpec, type MetricEncoding, type MetricField, type MetricSpec, type PaginatedResponse$1 as PaginatedResponse, type PaginationInfo$1 as PaginationInfo, type PaginationQuery$1 as PaginationQuery, type ParamRecord, type ParamValue, type PipelineTrace, PostgresAdapter, type PostgresAdapterOptions, type PostgresClientFn, type PostgresWireDialect, QueryErrorCode, QueryErrorCode as QueryErrorCodeType, QueryPanelSdkAPI, type QueryPanelSdkAPIOptions, QueryPipelineError, type RequestHandler, type RequestHandlerOptions, type RequestHandlerResult, type SchemaIngestBatchEmbedResponse, type SchemaIngestRequestOptions, type SchemaIntrospection, type SchemaSyncOptions, type SchemaSyncProgressEvent, SchemaTooLargeError, type SdkActiveChart, type SdkChart, type SdkDashboard, type SdkSession, type SdkSessionTurn, type SessionGetOptions, type SessionListOptions, type SessionUpdateInput, type SqlModifications, type StackingMode, type StageSchemaIngestOptions, type StageSchemaIngestResult, type SyncSchemaInBatchesOptions, type TableColumn, type TableEncoding, type TableSpec, type TenantSchemaAttachOptions, type TenantSchemaConfig, TenantSchemaError, type TimeUnit, type ValueFormat, type VizModifications, type VizSpec, type VizSpecGenerateInput, type VizSpecGenerateOptions, type VizSpecGeneratorInput, type VizSpecKind, type VizSpecResponse, type VizSpecResult, anonymizeResults, mapGeneratedParams, normalizeSqlParamsInput, parseTenantSchemaTemplate, resolveQueryPipeline, resolveTenantSchema, rewriteTenantSchema };
package/dist/index.d.ts CHANGED
@@ -230,6 +230,29 @@ declare class ClickHouseAdapter implements DatabaseAdapter {
230
230
  private extractRows;
231
231
  }
232
232
 
233
+ /** Attach option for schema-per-tenant isolation. */
234
+ interface TenantSchemaConfig {
235
+ /** Schema name with one `{tenant}` placeholder, e.g. `group_{tenant}_indexed`. */
236
+ template: string;
237
+ /** Extra schemas every tenant may read (lookup tables etc.). */
238
+ sharedSchemas?: string[];
239
+ }
240
+ declare class TenantSchemaError extends Error {
241
+ constructor(message: string);
242
+ }
243
+ interface ParsedTemplate {
244
+ prefix: string;
245
+ suffix: string;
246
+ }
247
+ declare function parseTenantSchemaTemplate(template: string): ParsedTemplate;
248
+ /** The schema a tenant's queries run against. Throws for unsafe tenant ids. */
249
+ declare function resolveTenantSchema(template: string, tenantId: string): string;
250
+ /**
251
+ * Rewrite every template-schema qualifier to the tenant's schema. Fails closed when
252
+ * another tenant's schema is still named anywhere (strings and comments included).
253
+ */
254
+ declare function rewriteTenantSchema(sql: string, template: string, tenantId: string): string;
255
+
233
256
  interface PostgresQueryResult {
234
257
  rows: Array<Record<string, unknown>>;
235
258
  fields: Array<{
@@ -257,6 +280,12 @@ interface PostgresAdapterOptions {
257
280
  * to these tables only.
258
281
  */
259
282
  allowedTables?: string[];
283
+ /**
284
+ * Schema-per-tenant connection: SQL has been rewritten to the caller's tenant
285
+ * schema, so queries may read any schema the template produces (or the tables
286
+ * `allowedTables` names in it) plus `sharedSchemas`, and nothing else.
287
+ */
288
+ tenantSchema?: TenantSchemaConfig;
260
289
  }
261
290
  /**
262
291
  * Simplified PostgreSQL adapter following IngestRequest format
@@ -270,6 +299,8 @@ declare class PostgresAdapter implements DatabaseAdapter {
270
299
  private readonly kind;
271
300
  private readonly dialect;
272
301
  private readonly allowedTables?;
302
+ /** Undefined when queries are unrestricted. */
303
+ private readonly allowsRelation?;
273
304
  constructor(clientFn: PostgresClientFn, options?: PostgresAdapterOptions);
274
305
  execute(sql: string, params?: Record<string, string | number | boolean | string[] | number[]>): Promise<DatabaseExecutionResult>;
275
306
  private validateQueryTables;
@@ -867,6 +898,12 @@ interface AskOptions {
867
898
  previousSql?: string;
868
899
  /** Maximum number of retry attempts on execution failure. */
869
900
  maxRetry?: number;
901
+ /**
902
+ * Also retry (within `maxRetry`) when the query returns no rows, or one row of only zeros/NULLs
903
+ * (what `COUNT(*) ... WHERE status = 'approved'` returns when the data stores 'APPROVE').
904
+ * Only that fact is sent to QueryPanel, never rows or values. Default false.
905
+ */
906
+ retryOnEmptyResult?: boolean;
870
907
  /** Maximum number of retries for chart generation. */
871
908
  chartMaxRetries?: number;
872
909
  /** Choose chart generation method. */
@@ -1321,6 +1358,16 @@ declare function mapGeneratedParams(params: Array<Record<string, unknown>>): Par
1321
1358
  */
1322
1359
  declare function normalizeSqlParamsInput(params: unknown[] | Record<string, unknown> | null | undefined): ParamRecord;
1323
1360
 
1361
+ /** Schema-per-tenant option for attachPostgres / attachRedshift (instead of tenantFieldName). */
1362
+ interface TenantSchemaAttachOptions {
1363
+ /**
1364
+ * Each tenant has its own schema, e.g. `{ template: "group_{tenant}_indexed" }`.
1365
+ * SQL naming any schema the template produces runs against the caller's tenant
1366
+ * schema, and only that schema (plus `sharedSchemas`) may be read.
1367
+ */
1368
+ tenantSchema?: TenantSchemaConfig;
1369
+ }
1370
+
1324
1371
  /** Options when constructing the SDK with HTTP (default) or with a custom API implementation. */
1325
1372
  interface QueryPanelSdkAPIOptions {
1326
1373
  defaultTenantId?: string;
@@ -1400,7 +1447,7 @@ declare class QueryPanelSdkAPI {
1400
1447
  tenantFieldName?: string;
1401
1448
  tenantFieldType?: string;
1402
1449
  enforceTenantIsolation?: boolean;
1403
- }): void;
1450
+ } & TenantSchemaAttachOptions): void;
1404
1451
  /**
1405
1452
  * Attach an Amazon Redshift database. Redshift speaks the Postgres wire
1406
1453
  * protocol, so `clientFn` can wrap a `pg` client; introspection uses
@@ -1412,7 +1459,7 @@ declare class QueryPanelSdkAPI {
1412
1459
  tenantFieldName?: string;
1413
1460
  tenantFieldType?: string;
1414
1461
  enforceTenantIsolation?: boolean;
1415
- }): void;
1462
+ } & TenantSchemaAttachOptions): void;
1416
1463
  attachBigQuery(name: string, clientFn: BigQueryClientFn, options: BigQueryAdapterOptions & {
1417
1464
  description?: string;
1418
1465
  tags?: string[];
@@ -2187,4 +2234,4 @@ declare class QueryPanelSdkAPI {
2187
2234
  }, signal?: AbortSignal): Promise<SdkDashboard[]>;
2188
2235
  }
2189
2236
 
2190
- export { ALL_VIZ_CHART_TYPES, type ActiveChartBulkGetResponse, type ActiveChartCreateInput, type ActiveChartListOptions, type ActiveChartUpdateInput, type AggregateOp, type AskOptions, type AskResponse, type AxisField, type AxisFieldInput, BigQueryAdapter, type BigQueryAdapterOptions, type BigQueryClientFn, type BigQueryQueryRequest, type BigQueryQueryResult, CallbackApiClient, type CatalogSchema, type CatalogTable, type CatalogTableType, type ChartBulkGetResponse, type ChartCreateInput, type ChartEncoding, type ChartEnvelope, type ChartListAllOptions, type ChartListOptions, type ChartModifyInput, type ChartModifyOptions, type ChartModifyResponse, type ChartSpec, type ChartType, type ChartUpdateInput, ClickHouseAdapter, type ClickHouseAdapterOptions, type ClickHouseClientFn, type ContextDocument, type DashboardCreateInput, type DashboardForkInput, type DashboardForkUpdateInput, type DashboardListOptions, type DashboardUpdateInput, type DatabaseAdapter, type DatabaseDialect, type DateRangeInput, type EncodingHints, type FieldRef, type FieldRefInput, type FieldType, type IQueryPanelApi, type IngestResponse, type IntentResult, type ListTablesOptions, type MapEncoding, type MapLocation, type MapLocationMode, type MapSpec, type MetricEncoding, type MetricField, type MetricSpec, type PaginatedResponse$1 as PaginatedResponse, type PaginationInfo$1 as PaginationInfo, type PaginationQuery$1 as PaginationQuery, type ParamRecord, type ParamValue, type PipelineTrace, PostgresAdapter, type PostgresAdapterOptions, type PostgresClientFn, type PostgresWireDialect, QueryErrorCode, QueryErrorCode as QueryErrorCodeType, QueryPanelSdkAPI, type QueryPanelSdkAPIOptions, QueryPipelineError, type RequestHandler, type RequestHandlerOptions, type RequestHandlerResult, type SchemaIngestBatchEmbedResponse, type SchemaIngestRequestOptions, type SchemaIntrospection, type SchemaSyncOptions, type SchemaSyncProgressEvent, SchemaTooLargeError, type SdkActiveChart, type SdkChart, type SdkDashboard, type SdkSession, type SdkSessionTurn, type SessionGetOptions, type SessionListOptions, type SessionUpdateInput, type SqlModifications, type StackingMode, type StageSchemaIngestOptions, type StageSchemaIngestResult, type SyncSchemaInBatchesOptions, type TableColumn, type TableEncoding, type TableSpec, type TimeUnit, type ValueFormat, type VizModifications, type VizSpec, type VizSpecGenerateInput, type VizSpecGenerateOptions, type VizSpecGeneratorInput, type VizSpecKind, type VizSpecResponse, type VizSpecResult, anonymizeResults, mapGeneratedParams, normalizeSqlParamsInput, resolveQueryPipeline };
2237
+ export { ALL_VIZ_CHART_TYPES, type ActiveChartBulkGetResponse, type ActiveChartCreateInput, type ActiveChartListOptions, type ActiveChartUpdateInput, type AggregateOp, type AskOptions, type AskResponse, type AxisField, type AxisFieldInput, BigQueryAdapter, type BigQueryAdapterOptions, type BigQueryClientFn, type BigQueryQueryRequest, type BigQueryQueryResult, CallbackApiClient, type CatalogSchema, type CatalogTable, type CatalogTableType, type ChartBulkGetResponse, type ChartCreateInput, type ChartEncoding, type ChartEnvelope, type ChartListAllOptions, type ChartListOptions, type ChartModifyInput, type ChartModifyOptions, type ChartModifyResponse, type ChartSpec, type ChartType, type ChartUpdateInput, ClickHouseAdapter, type ClickHouseAdapterOptions, type ClickHouseClientFn, type ContextDocument, type DashboardCreateInput, type DashboardForkInput, type DashboardForkUpdateInput, type DashboardListOptions, type DashboardUpdateInput, type DatabaseAdapter, type DatabaseDialect, type DateRangeInput, type EncodingHints, type FieldRef, type FieldRefInput, type FieldType, type IQueryPanelApi, type IngestResponse, type IntentResult, type ListTablesOptions, type MapEncoding, type MapLocation, type MapLocationMode, type MapSpec, type MetricEncoding, type MetricField, type MetricSpec, type PaginatedResponse$1 as PaginatedResponse, type PaginationInfo$1 as PaginationInfo, type PaginationQuery$1 as PaginationQuery, type ParamRecord, type ParamValue, type PipelineTrace, PostgresAdapter, type PostgresAdapterOptions, type PostgresClientFn, type PostgresWireDialect, QueryErrorCode, QueryErrorCode as QueryErrorCodeType, QueryPanelSdkAPI, type QueryPanelSdkAPIOptions, QueryPipelineError, type RequestHandler, type RequestHandlerOptions, type RequestHandlerResult, type SchemaIngestBatchEmbedResponse, type SchemaIngestRequestOptions, type SchemaIntrospection, type SchemaSyncOptions, type SchemaSyncProgressEvent, SchemaTooLargeError, type SdkActiveChart, type SdkChart, type SdkDashboard, type SdkSession, type SdkSessionTurn, type SessionGetOptions, type SessionListOptions, type SessionUpdateInput, type SqlModifications, type StackingMode, type StageSchemaIngestOptions, type StageSchemaIngestResult, type SyncSchemaInBatchesOptions, type TableColumn, type TableEncoding, type TableSpec, type TenantSchemaAttachOptions, type TenantSchemaConfig, TenantSchemaError, type TimeUnit, type ValueFormat, type VizModifications, type VizSpec, type VizSpecGenerateInput, type VizSpecGenerateOptions, type VizSpecGeneratorInput, type VizSpecKind, type VizSpecResponse, type VizSpecResult, anonymizeResults, mapGeneratedParams, normalizeSqlParamsInput, parseTenantSchemaTemplate, resolveQueryPipeline, resolveTenantSchema, rewriteTenantSchema };
package/dist/index.js CHANGED
@@ -581,6 +581,248 @@ function toNumber(value) {
581
581
  return Number.isNaN(parsed) ? void 0 : parsed;
582
582
  }
583
583
 
584
+ // src/core/sql-relations.ts
585
+ function tokenize(sql) {
586
+ const tokens = [];
587
+ let i = 0;
588
+ while (i < sql.length) {
589
+ const ch = sql[i];
590
+ const next = sql[i + 1];
591
+ if (/\s/.test(ch)) {
592
+ i++;
593
+ } else if (ch === "-" && next === "-") {
594
+ while (i < sql.length && sql[i] !== "\n") i++;
595
+ } else if (ch === "/" && next === "*") {
596
+ const end = sql.indexOf("*/", i + 2);
597
+ i = end < 0 ? sql.length : end + 2;
598
+ } else if (ch === "'") {
599
+ i++;
600
+ while (i < sql.length) {
601
+ if (sql[i] === "\\") i += 2;
602
+ else if (sql[i] === "'" && sql[i + 1] === "'") i += 2;
603
+ else if (sql[i] === "'") break;
604
+ else i++;
605
+ }
606
+ i++;
607
+ } else if (ch === "$" && /[A-Za-z_$]/.test(next ?? "")) {
608
+ const tag = /^\$[A-Za-z_]*\$/.exec(sql.slice(i))?.[0];
609
+ if (tag) {
610
+ const end = sql.indexOf(tag, i + tag.length);
611
+ i = end < 0 ? sql.length : end + tag.length;
612
+ } else {
613
+ i++;
614
+ }
615
+ } else if (ch === '"') {
616
+ const start = i;
617
+ let text = "";
618
+ i++;
619
+ while (i < sql.length) {
620
+ if (sql[i] === '"' && sql[i + 1] === '"') {
621
+ text += '"';
622
+ i += 2;
623
+ } else if (sql[i] === '"') break;
624
+ else text += sql[i++];
625
+ }
626
+ i++;
627
+ tokens.push({ kind: "word", text, quoted: true, start, end: i });
628
+ } else if (/[A-Za-z_]/.test(ch)) {
629
+ const start = i;
630
+ while (i < sql.length && /[A-Za-z0-9_$]/.test(sql[i])) i++;
631
+ tokens.push({ kind: "word", text: sql.slice(start, i), quoted: false, start, end: i });
632
+ } else if (/[0-9]/.test(ch)) {
633
+ while (i < sql.length && /[0-9.eE]/.test(sql[i])) i++;
634
+ } else {
635
+ tokens.push({ kind: "punct", text: ch, start: i, end: i + 1 });
636
+ i++;
637
+ }
638
+ }
639
+ return tokens;
640
+ }
641
+ var identifier = (t) => t?.kind === "word" ? t.quoted ? t.text : t.text.toLowerCase() : void 0;
642
+ var isKeyword = (t, word) => t?.kind === "word" && !t.quoted && t.text.toLowerCase() === word;
643
+ var isPunct = (t, p) => t?.kind === "punct" && t.text === p;
644
+ var CLAUSE_WORDS = /* @__PURE__ */ new Set([
645
+ "where",
646
+ "group",
647
+ "order",
648
+ "limit",
649
+ "having",
650
+ "union",
651
+ "intersect",
652
+ "except",
653
+ "join",
654
+ "inner",
655
+ "left",
656
+ "right",
657
+ "full",
658
+ "cross",
659
+ "natural",
660
+ "on",
661
+ "using",
662
+ "window",
663
+ "qualify",
664
+ "offset",
665
+ "fetch",
666
+ "for",
667
+ "lateral",
668
+ "as"
669
+ ]);
670
+ var FROM_CLAUSE_END = /* @__PURE__ */ new Set([
671
+ "where",
672
+ "group",
673
+ "having",
674
+ "order",
675
+ "limit",
676
+ "offset",
677
+ "fetch",
678
+ "union",
679
+ "intersect",
680
+ "except",
681
+ "window",
682
+ "qualify",
683
+ "for",
684
+ "returning"
685
+ ]);
686
+ function cteNames(tokens) {
687
+ const names = /* @__PURE__ */ new Set();
688
+ for (let k = 0; k + 2 < tokens.length; k++) {
689
+ const value = identifier(tokens[k]);
690
+ if (!value) continue;
691
+ if (isKeyword(tokens[k + 1], "as") && isPunct(tokens[k + 2], "(")) names.add(value);
692
+ if (isPunct(tokens[k + 1], "(")) {
693
+ let j = k + 2;
694
+ while (j < tokens.length && !isPunct(tokens[j], ")")) j++;
695
+ if (isKeyword(tokens[j + 1], "as") && isPunct(tokens[j + 2], "(")) names.add(value);
696
+ }
697
+ }
698
+ return names;
699
+ }
700
+ function findRelations(sql) {
701
+ const tokens = tokenize(sql);
702
+ const ctes = cteNames(tokens);
703
+ const relations = [];
704
+ const readItem = (start) => {
705
+ let k = start;
706
+ if (isKeyword(tokens[k], "lateral")) k++;
707
+ const first = identifier(tokens[k]);
708
+ if (!first) return k;
709
+ const parts = [first];
710
+ k++;
711
+ while (isPunct(tokens[k], ".") && identifier(tokens[k + 1])) {
712
+ parts.push(identifier(tokens[k + 1]));
713
+ k += 2;
714
+ }
715
+ if (isPunct(tokens[k], "(")) return k;
716
+ if (!(parts.length === 1 && ctes.has(first))) relations.push({ parts });
717
+ if (isKeyword(tokens[k], "as")) k++;
718
+ const alias = tokens[k];
719
+ if (alias?.kind === "word" && (alias.quoted || !CLAUSE_WORDS.has(alias.text.toLowerCase()))) k++;
720
+ return k;
721
+ };
722
+ const selectAtDepth = [false];
723
+ const inFromAtDepth = [false];
724
+ const top = () => selectAtDepth.length - 1;
725
+ for (let k = 0; k < tokens.length; k++) {
726
+ const token = tokens[k];
727
+ if (isPunct(token, "(")) {
728
+ selectAtDepth.push(false);
729
+ inFromAtDepth.push(false);
730
+ } else if (isPunct(token, ")")) {
731
+ if (selectAtDepth.length > 1) {
732
+ selectAtDepth.pop();
733
+ inFromAtDepth.pop();
734
+ }
735
+ } else if (isKeyword(token, "select")) {
736
+ selectAtDepth[top()] = true;
737
+ inFromAtDepth[top()] = false;
738
+ } else if (isKeyword(token, "join")) {
739
+ k = readItem(k + 1) - 1;
740
+ } else if (isKeyword(token, "from") && selectAtDepth[top()] && !isKeyword(tokens[k - 1], "distinct")) {
741
+ inFromAtDepth[top()] = true;
742
+ k = readItem(k + 1) - 1;
743
+ } else if (isPunct(token, ",") && inFromAtDepth[top()]) {
744
+ k = readItem(k + 1) - 1;
745
+ } else if (token?.kind === "word" && !token.quoted && FROM_CLAUSE_END.has(token.text.toLowerCase())) {
746
+ inFromAtDepth[top()] = false;
747
+ }
748
+ }
749
+ return relations;
750
+ }
751
+
752
+ // src/core/tenant-schema.ts
753
+ var TenantSchemaError = class extends Error {
754
+ constructor(message) {
755
+ super(message);
756
+ this.name = "TenantSchemaError";
757
+ }
758
+ };
759
+ var PLACEHOLDER = "{tenant}";
760
+ var TEMPLATE_PART = /^[a-z0-9_]*$/;
761
+ var TENANT_ID = /^[A-Za-z0-9_]{1,64}$/;
762
+ function parseTenantSchemaTemplate(template) {
763
+ const value = template.trim().toLowerCase();
764
+ const index = value.indexOf(PLACEHOLDER);
765
+ if (index < 0 || value.indexOf(PLACEHOLDER, index + 1) >= 0) {
766
+ throw new TenantSchemaError(
767
+ `Tenant schema template must contain ${PLACEHOLDER} exactly once`
768
+ );
769
+ }
770
+ const prefix = value.slice(0, index);
771
+ const suffix = value.slice(index + PLACEHOLDER.length);
772
+ if (!TEMPLATE_PART.test(prefix) || !TEMPLATE_PART.test(suffix) || !(prefix || suffix)) {
773
+ throw new TenantSchemaError(
774
+ "Tenant schema template may only use a-z, 0-9 and _ around {tenant}"
775
+ );
776
+ }
777
+ return { prefix, suffix };
778
+ }
779
+ function matchesTemplate({ prefix, suffix }, schema) {
780
+ const value = schema.toLowerCase();
781
+ return value.length > prefix.length + suffix.length && value.startsWith(prefix) && value.endsWith(suffix) && /^[a-z0-9_]+$/.test(value.slice(prefix.length, value.length - suffix.length));
782
+ }
783
+ function resolveTenantSchema(template, tenantId) {
784
+ const { prefix, suffix } = parseTenantSchemaTemplate(template);
785
+ const id = tenantId.trim();
786
+ if (!TENANT_ID.test(id)) {
787
+ throw new TenantSchemaError(
788
+ "tenantId must be 1-64 characters of letters, digits or _ for schema-per-tenant isolation"
789
+ );
790
+ }
791
+ return `${prefix}${id.toLowerCase()}${suffix}`;
792
+ }
793
+ function rewriteTenantSchema(sql, template, tenantId) {
794
+ const parsed = parseTenantSchemaTemplate(template);
795
+ const tenantSchema = resolveTenantSchema(template, tenantId);
796
+ const tokens = tokenize(sql);
797
+ let rewritten = "";
798
+ let cursor = 0;
799
+ for (let k = 0; k < tokens.length; k++) {
800
+ const token = tokens[k];
801
+ const value = identifier(token);
802
+ if (!token || !value || !isPunct(tokens[k + 1], ".")) continue;
803
+ if (!matchesTemplate(parsed, value) || value.toLowerCase() === tenantSchema) continue;
804
+ const replacement = token.kind === "word" && token.quoted ? `"${tenantSchema}"` : tenantSchema;
805
+ rewritten += sql.slice(cursor, token.start) + replacement;
806
+ cursor = token.end;
807
+ }
808
+ rewritten += sql.slice(cursor);
809
+ const pattern = new RegExp(`${escapeRegex(parsed.prefix)}[a-z0-9_]+${escapeRegex(parsed.suffix)}`, "gi");
810
+ for (const match of rewritten.matchAll(pattern)) {
811
+ if (match[0].toLowerCase() !== tenantSchema) {
812
+ throw new TenantSchemaError("Query references another tenant's schema");
813
+ }
814
+ }
815
+ return rewritten;
816
+ }
817
+ var TENANT_SCHEMA_KEY = PLACEHOLDER;
818
+ function tenantSchemaKey(template) {
819
+ const parsed = parseTenantSchemaTemplate(template);
820
+ return (schema) => matchesTemplate(parsed, schema) ? TENANT_SCHEMA_KEY : schema.toLowerCase();
821
+ }
822
+ function escapeRegex(value) {
823
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
824
+ }
825
+
584
826
  // src/adapters/postgres.ts
585
827
  var WILDCARD = "*";
586
828
  function maxPostgresPlaceholderIndex(sql) {
@@ -627,6 +869,11 @@ var PostgresAdapter = class {
627
869
  this.defaultSchema
628
870
  );
629
871
  }
872
+ this.allowsRelation = buildRelationAllowlist(
873
+ this.allowedTables,
874
+ options.tenantSchema,
875
+ this.defaultSchema
876
+ );
630
877
  }
631
878
  clientFn;
632
879
  databaseName;
@@ -634,9 +881,11 @@ var PostgresAdapter = class {
634
881
  kind;
635
882
  dialect;
636
883
  allowedTables;
884
+ /** Undefined when queries are unrestricted. */
885
+ allowsRelation;
637
886
  async execute(sql, params) {
638
- if (this.allowedTables) {
639
- this.validateQueryTables(sql);
887
+ if (this.allowsRelation) {
888
+ this.validateQueryTables(sql, this.allowsRelation);
640
889
  }
641
890
  const boundParams = filterParamsForPostgresSql(sql, params);
642
891
  let paramArray;
@@ -647,29 +896,11 @@ var PostgresAdapter = class {
647
896
  const fields = result.fields.map((f) => f.name);
648
897
  return { fields, rows: result.rows };
649
898
  }
650
- validateQueryTables(sql) {
651
- if (!this.allowedTables || this.allowedTables.length === 0) {
652
- return;
653
- }
654
- const allowedSet = new Set(
655
- this.allowedTables.map((t) => tableKey2(t.schema, t.table))
656
- );
657
- const allowedSchemas = new Set(
658
- this.allowedTables.filter((t) => t.table === WILDCARD).map((t) => t.schema)
659
- );
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*/)");
661
- const tablePattern = /(?:FROM|JOIN)\s+(?:ONLY\s+)?(?:([a-zA-Z_][a-zA-Z0-9_]*)\.)?(["']?[a-zA-Z_][a-zA-Z0-9_]*["']?)/gi;
662
- const matches = neutralizedSql.matchAll(tablePattern);
663
- for (const match of matches) {
664
- const schema = match[1] ?? this.defaultSchema;
665
- const table = match[2]?.replace(/['"]/g, "");
666
- if (table) {
667
- const key = tableKey2(schema, table);
668
- if (!allowedSet.has(key) && !allowedSchemas.has(schema)) {
669
- throw new Error(
670
- `Query references table "${schema}.${table}" which is not in the allowed tables list`
671
- );
672
- }
899
+ validateQueryTables(sql, allows) {
900
+ for (const relation of findRelations(sql)) {
901
+ if (!allows(relation)) {
902
+ const name = relation.parts.length === 1 ? `${this.defaultSchema}.${relation.parts[0]}` : relation.parts.join(".");
903
+ throw new Error(`Query references table "${name}" which is not in the allowed tables list`);
673
904
  }
674
905
  }
675
906
  }
@@ -707,6 +938,9 @@ var PostgresAdapter = class {
707
938
  return positionalParams;
708
939
  }
709
940
  async validate(sql, params) {
941
+ if (this.allowsRelation) {
942
+ this.validateQueryTables(sql, this.allowsRelation);
943
+ }
710
944
  const boundParams = filterParamsForPostgresSql(sql, params);
711
945
  let paramArray;
712
946
  if (boundParams) {
@@ -996,6 +1230,27 @@ function buildListTablesQuery(dialect, options) {
996
1230
  LIMIT ${limit};`;
997
1231
  return { sql, params };
998
1232
  }
1233
+ function buildRelationAllowlist(allowedTables, tenantSchema, defaultSchema) {
1234
+ if (!allowedTables?.length && !tenantSchema) return void 0;
1235
+ const schemaKey = tenantSchema ? tenantSchemaKey(tenantSchema.template) : (schema) => schema.toLowerCase();
1236
+ const entries = [
1237
+ ...allowedTables?.length ? allowedTables : [{ schema: TENANT_SCHEMA_KEY, table: WILDCARD }],
1238
+ ...(tenantSchema?.sharedSchemas ?? []).map((schema) => ({ schema, table: WILDCARD }))
1239
+ ];
1240
+ const tables = /* @__PURE__ */ new Set();
1241
+ const wildcardSchemas = /* @__PURE__ */ new Set();
1242
+ for (const entry of entries) {
1243
+ const schema = entry.schema === TENANT_SCHEMA_KEY ? entry.schema : schemaKey(entry.schema);
1244
+ if (entry.table === WILDCARD) wildcardSchemas.add(schema);
1245
+ else tables.add(tableKey2(schema, entry.table.toLowerCase()));
1246
+ }
1247
+ return ({ parts }) => {
1248
+ if (parts.length > 2) return false;
1249
+ const table = parts[parts.length - 1].toLowerCase();
1250
+ const schema = schemaKey(parts.length === 2 ? parts[0] : defaultSchema);
1251
+ return wildcardSchemas.has(schema) || tables.has(tableKey2(schema, table));
1252
+ };
1253
+ }
999
1254
  function tableKey2(schema, table) {
1000
1255
  return `${schema}.${table}`;
1001
1256
  }
@@ -1600,7 +1855,9 @@ var QueryEngine = class {
1600
1855
  const adapter = this.getDatabase(databaseName);
1601
1856
  const metadata = this.getDatabaseMetadata(databaseName);
1602
1857
  let finalSql = sql;
1603
- if (metadata) {
1858
+ if (metadata?.tenantSchema) {
1859
+ finalSql = rewriteTenantSchema(sql, metadata.tenantSchema.template, tenantId);
1860
+ } else if (metadata) {
1604
1861
  finalSql = this.ensureTenantIsolation(sql, params, metadata, tenantId);
1605
1862
  }
1606
1863
  await adapter.validate(finalSql, params);
@@ -2444,6 +2701,15 @@ function resolveQueryPipeline(pipeline) {
2444
2701
  }
2445
2702
  return "v2";
2446
2703
  }
2704
+ var EMPTY_RESULT_ERROR = "The query ran but returned no rows (or only zero/NULL values). A filter value probably does not match how the data stores it (case, spelling, codes such as 'APPROVE' vs 'approved'), or a filter is narrower than the question. Check each filter value against the column's documented values and fix it; keep every filter the question asks for.";
2705
+ function isEmptyLikeResult(rows) {
2706
+ if (rows.length === 0) return true;
2707
+ if (rows.length > 1) return false;
2708
+ const row = rows[0];
2709
+ if (!row || typeof row !== "object") return false;
2710
+ const values = Object.values(row);
2711
+ return values.length > 0 && values.every((v) => v === null || v === 0 || v === "0" || v === 0n);
2712
+ }
2447
2713
  async function ask(client, queryEngine, question, options, signal) {
2448
2714
  const tenantId = resolveTenantId5(client, options.tenantId);
2449
2715
  const sessionId = crypto3.randomUUID();
@@ -2526,6 +2792,17 @@ async function ask(client, queryEngine, question, options, signal) {
2526
2792
  tenantId
2527
2793
  );
2528
2794
  const rows = execution.rows ?? [];
2795
+ if (options.retryOnEmptyResult && attempt < maxRetry && isEmptyLikeResult(rows)) {
2796
+ attempt++;
2797
+ lastError = EMPTY_RESULT_ERROR;
2798
+ previousSql = sql;
2799
+ console.warn("Query returned no data, retrying query generation", {
2800
+ attempt,
2801
+ maxAttempts: maxRetry + 1,
2802
+ sql
2803
+ });
2804
+ continue;
2805
+ }
2529
2806
  const chartType = options.chartType ?? "vega-lite";
2530
2807
  let chart = {
2531
2808
  specType: chartType,
@@ -3329,6 +3606,9 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
3329
3606
  this.queryEngine.attachDatabase(name, adapter, metadata);
3330
3607
  }
3331
3608
  attachPostgres(name, clientFn, options) {
3609
+ if (options?.tenantSchema && options.tenantFieldName) {
3610
+ throw new Error("Use either tenantSchema or tenantFieldName for tenant isolation, not both");
3611
+ }
3332
3612
  const adapter = new PostgresAdapter(clientFn, options);
3333
3613
  const metadata = {
3334
3614
  name,
@@ -3340,7 +3620,8 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
3340
3620
  enforceTenantIsolation: resolveAttachEnforceTenantIsolation(
3341
3621
  options?.tenantFieldName,
3342
3622
  options?.enforceTenantIsolation
3343
- )
3623
+ ),
3624
+ tenantSchema: options?.tenantSchema
3344
3625
  };
3345
3626
  this.queryEngine.attachDatabase(name, adapter, metadata);
3346
3627
  }
@@ -4394,9 +4675,13 @@ export {
4394
4675
  QueryPanelSdkAPI,
4395
4676
  QueryPipelineError,
4396
4677
  SchemaTooLargeError,
4678
+ TenantSchemaError,
4397
4679
  anonymizeResults,
4398
4680
  mapGeneratedParams,
4399
4681
  normalizeSqlParamsInput,
4400
- resolveQueryPipeline
4682
+ parseTenantSchemaTemplate,
4683
+ resolveQueryPipeline,
4684
+ resolveTenantSchema,
4685
+ rewriteTenantSchema
4401
4686
  };
4402
4687
  //# sourceMappingURL=index.js.map