hyperdx/packages/common-utils/src/rawSqlParams.ts
Drew Davis 1d83bebb54
feat: Add support for dashboard filters on Raw SQL Charts (#1924)
## Summary

This PR updates Raw SQL charts with support for dashboard filters, using the $__filters macro.

Lucene global filters require a Source to be included in the ChartConfig, for schema introspection and the `implicitColumnExpression` value. To support Lucene filters, this PR also updates the RawSqlChartConfig type to include optional `source`, `implicitColumnExpression`, and `from` properties. Only `source` is saved in the database. The external API has been updated to accept the `source` field for raw SQL charts as well.

Dashboard import/export has also been updated to support source mapping for raw sql charts with sources.

### Screenshots or video

Both the global filter and the drop-down filters are applied:

<img width="683" height="574" alt="Screenshot 2026-03-17 at 10 57 36 AM" src="https://github.com/user-attachments/assets/280ba0b5-55f7-4107-a55c-eeb1497ac7de" />

To render Lucene conditions, we require Source information (the `from` and `implicitColumnExpression` fields). When a source is not set, filters are therefore not applied:

<img width="782" height="618" alt="Screenshot 2026-03-17 at 10 54 41 AM" src="https://github.com/user-attachments/assets/3ad19ea7-12ee-4334-abe2-8985a0be952c" />

Similarly, if the `$__filters` macro is not used, then the filters are not applied

<img width="704" height="292" alt="Screenshot 2026-03-17 at 10 56 33 AM" src="https://github.com/user-attachments/assets/e1169e4a-2f64-4cd2-bc05-f699fecef8c1" />

Import:

<img width="993" height="669" alt="Screenshot 2026-03-17 at 3 35 16 PM" src="https://github.com/user-attachments/assets/6ebe20c0-19e2-4e90-95d0-8b02c2af0612" />

### How to test locally or on Vercel

This can be tested in the preview environment.
1. Create a saved dashboard and add some filters.
2. Create a raw sql tile, with or without the $__filters macro
3. Try selecting filters, deselecting filters, and applying global SQL and Lucene filters
4. Inspect the generated queries in the console.

### References



- Linear Issue: Closes HDX-3694
- Related PRs:
2026-03-18 12:03:27 +00:00

118 lines
4 KiB
TypeScript

import { ChSql } from './clickhouse';
import {
convertDateRangeToGranularityString,
convertGranularityToSeconds,
} from './core/utils';
import { DateRange, DisplayType, RawSqlChartConfig } from './types';
type QueryParamDefinition = {
name: string;
type: string;
description: string;
get: (config: RawSqlChartConfig & Partial<DateRange>) => any;
};
const getIntervalSeconds = (config: RawSqlChartConfig & Partial<DateRange>) => {
const granularity = config.granularity ?? 'auto';
const effectiveGranularity =
granularity === 'auto' && config.dateRange
? convertDateRangeToGranularityString(config.dateRange)
: granularity;
return convertGranularityToSeconds(effectiveGranularity);
};
export const QUERY_PARAMS: Record<string, QueryParamDefinition> = {
startDateMilliseconds: {
name: 'startDateMilliseconds',
type: 'Int64',
description:
'start of the dashboard date range, in milliseconds since epoch',
get: (config: RawSqlChartConfig & Partial<DateRange>) =>
config.dateRange ? config.dateRange[0].getTime() : undefined,
},
endDateMilliseconds: {
name: 'endDateMilliseconds',
type: 'Int64',
description: 'end of the dashboard date range, in milliseconds since epoch',
get: (config: RawSqlChartConfig & Partial<DateRange>) =>
config.dateRange ? config.dateRange[1].getTime() : undefined,
},
intervalSeconds: {
name: 'intervalSeconds',
type: 'Int64',
description: 'time bucket size in seconds',
get: getIntervalSeconds,
},
intervalMilliseconds: {
name: 'intervalMilliseconds',
type: 'Int64',
description: 'time bucket size in milliseconds',
get: (config: RawSqlChartConfig & Partial<DateRange>) =>
getIntervalSeconds(config) * 1000,
},
};
export const QUERY_PARAMS_BY_DISPLAY_TYPE: Record<
DisplayType,
QueryParamDefinition[]
> = {
[DisplayType.Line]: [
QUERY_PARAMS.startDateMilliseconds,
QUERY_PARAMS.endDateMilliseconds,
QUERY_PARAMS.intervalSeconds,
QUERY_PARAMS.intervalMilliseconds,
],
[DisplayType.StackedBar]: [
QUERY_PARAMS.startDateMilliseconds,
QUERY_PARAMS.endDateMilliseconds,
QUERY_PARAMS.intervalSeconds,
QUERY_PARAMS.intervalMilliseconds,
],
[DisplayType.Table]: [
QUERY_PARAMS.startDateMilliseconds,
QUERY_PARAMS.endDateMilliseconds,
],
[DisplayType.Pie]: [
QUERY_PARAMS.startDateMilliseconds,
QUERY_PARAMS.endDateMilliseconds,
],
[DisplayType.Number]: [
QUERY_PARAMS.startDateMilliseconds,
QUERY_PARAMS.endDateMilliseconds,
],
[DisplayType.Search]: [],
[DisplayType.Heatmap]: [],
[DisplayType.Markdown]: [],
};
const TIME_CHART_EXAMPLE_SQL = `SELECT
toStartOfInterval(TimestampTime, INTERVAL {intervalSeconds:Int64} second) AS ts, -- (Timestamp column)
ServiceName, -- (Group name column)
count() -- (Series value column)
FROM otel_logs
WHERE TimestampTime >= fromUnixTimestamp64Milli ({startDateMilliseconds:Int64})
AND TimestampTime < fromUnixTimestamp64Milli ({endDateMilliseconds:Int64})
AND $__filters
GROUP BY ServiceName, ts`;
export const DATE_RANGE_WHERE_EXAMPLE_SQL = `WHERE TimestampTime >= fromUnixTimestamp64Milli ({startDateMilliseconds:Int64})
AND TimestampTime <= fromUnixTimestamp64Milli ({endDateMilliseconds:Int64})
AND $__filters`;
export const QUERY_PARAM_EXAMPLES: Record<DisplayType, string> = {
[DisplayType.Line]: TIME_CHART_EXAMPLE_SQL,
[DisplayType.StackedBar]: TIME_CHART_EXAMPLE_SQL,
[DisplayType.Table]: DATE_RANGE_WHERE_EXAMPLE_SQL,
[DisplayType.Pie]: DATE_RANGE_WHERE_EXAMPLE_SQL,
[DisplayType.Number]: DATE_RANGE_WHERE_EXAMPLE_SQL,
[DisplayType.Search]: '',
[DisplayType.Heatmap]: '',
[DisplayType.Markdown]: '',
};
export function renderQueryParam(name: keyof typeof QUERY_PARAMS): string {
// eslint-disable-next-line security/detect-object-injection
return `{${QUERY_PARAMS[name].name}:${QUERY_PARAMS[name].type}}`;
}