feat: Support ClickHouse datasource plugin macros in Raw SQL charts (#1922)

## Summary

This PR extends Raw SQL Charts to support [macros that are supported by the Grafana ClickHouse plugin](https://github.com/grafana/clickhouse-datasource?tab=readme-ov-file#macros).

Query Params and Macros are also now included as auto-complete suggestions in the SQL Editor.

### Screenshots or video

<img width="1434" height="1080" alt="Screenshot 2026-03-16 at 12 53 03 PM" src="https://github.com/user-attachments/assets/07f753e4-28f1-43f4-8add-f123dae0b12a" />

### How to test locally or on Vercel

This can be tested in Vercel preview - just reference the supported macros in a raw SQL chart.

### References



- Linear Issue: Closes HDX-3651
- Related PRs:
This commit is contained in:
Drew Davis 2026-03-17 13:23:14 -04:00 committed by GitHub
parent 74d925949c
commit 4cee5d698b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 556 additions and 21 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/common-utils": patch
---
feat: Support ClickHouse datasource plugin macros in Raw SQL chart configs

View file

@ -4,10 +4,13 @@ import {
TableConnection,
tcFromSource,
} from '@hyperdx/common-utils/dist/core/metadata';
import { MACRO_SUGGESTIONS } from '@hyperdx/common-utils/dist/macros';
import { QUERY_PARAMS_BY_DISPLAY_TYPE } from '@hyperdx/common-utils/dist/rawSqlParams';
import { DisplayType, SourceKind } from '@hyperdx/common-utils/dist/types';
import { Box, Button, Group, Stack, Text } from '@mantine/core';
import { SQLEditorControlled } from '@/components/SQLEditor/SQLEditor';
import { type SQLCompletion } from '@/components/SQLEditor/utils';
import useResizable from '@/hooks/useResizable';
import { useSources } from '@/source';
import { getAllMetricTables } from '@/utils';
@ -51,6 +54,29 @@ export default function RawSqlChartEditor({
const placeholderSQl = SQL_PLACEHOLDERS[displayType ?? DisplayType.Table];
const additionalCompletions: SQLCompletion[] = useMemo(() => {
const effectiveDisplayType = displayType ?? DisplayType.Table;
const params = QUERY_PARAMS_BY_DISPLAY_TYPE[effectiveDisplayType];
const paramCompletions: SQLCompletion[] = params.map(({ name, type }) => ({
label: `{${name}:${type}}`,
apply: `{${name}:${type}`, // Omit the closing } because the editor will have added it when the user types {
detail: 'param',
type: 'variable',
}));
const macroCompletions: SQLCompletion[] = MACRO_SUGGESTIONS.map(
({ name, argCount }) => ({
label: `$__${name}`,
apply: argCount > 0 ? `$__${name}(` : `$__${name}`,
detail: 'macro',
type: 'function',
}),
);
return [...paramCompletions, ...macroCompletions];
}, [displayType]);
const tableConnections: TableConnection[] = useMemo(() => {
if (!sources) return [];
return sources
@ -97,6 +123,7 @@ export default function RawSqlChartEditor({
enableLineWrapping
placeholder={placeholderSQl}
tableConnections={tableConnections}
additionalCompletions={additionalCompletions}
/>
<div className={resizeStyles.resizeYHandle} onMouseDown={startResize} />
</Box>

View file

@ -6,6 +6,7 @@ import {
import { DisplayType } from '@hyperdx/common-utils/dist/types';
import {
ActionIcon,
Anchor,
Code,
Collapse,
Group,
@ -97,7 +98,7 @@ export function RawSqlChartInstructions({
<Text size="xs" fw="bold">
The following parameters can be referenced in this chart's SQL:
</Text>
<List size="xs" withPadding spacing={3} mb="xs">
<List size="xs" withPadding spacing={3}>
{availableParams.map(({ name, type, description }) => (
<List.Item key={name}>
<ParamSnippet
@ -140,6 +141,16 @@ export function RawSqlChartInstructions({
<Code fz="xs" block>
{QUERY_PARAM_EXAMPLES[displayType]}
</Code>
<Text size="xs" mt="xs">
Macros from the{' '}
<Anchor
href="https://github.com/grafana/clickhouse-datasource?tab=readme-ov-file#macros"
target="_blank"
>
ClickHouse Datasource Grafana Plugin
</Anchor>{' '}
may also be used.
</Text>
</div>
</Stack>
</Collapse>

View file

@ -17,6 +17,7 @@ import {
createCodeMirrorSqlDialect,
createCodeMirrorStyleTheme,
DEFAULT_CODE_MIRROR_BASIC_SETUP,
type SQLCompletion,
} from './utils';
type SQLEditorProps = {
@ -26,6 +27,7 @@ type SQLEditorProps = {
height?: string;
enableLineWrapping?: boolean;
tableConnections?: TableConnection[];
additionalCompletions?: SQLCompletion[];
};
export default function SQLEditor({
@ -35,6 +37,7 @@ export default function SQLEditor({
height,
enableLineWrapping = false,
tableConnections,
additionalCompletions,
}: SQLEditorProps) {
const { colorScheme } = useMantineColorScheme();
const ref = useRef<ReactCodeMirrorRef>(null);
@ -66,13 +69,14 @@ export default function SQLEditor({
effects: compartmentRef.current.reconfigure(
createCodeMirrorSqlDialect({
identifiers,
additionalCompletions,
includeAggregateFunctions: true,
includeRegularFunctions: true,
}),
),
});
},
[fields, tableConnections],
[additionalCompletions, fields, tableConnections],
);
useEffect(() => {

View file

@ -11,20 +11,29 @@ import {
ALL_KEYWORDS,
REGULAR_FUNCTIONS,
} from './constants';
export type SQLCompletion = {
label: string;
apply?: string;
detail?: string;
type?: string;
};
/**
* Creates a custom CodeMirror completion source for SQL identifiers (column names, table
* names, functions, etc.) that inserts them verbatim, without quoting.
*/
function createIdentifierCompletionSource(completions: Completion[]) {
return (context: CompletionContext) => {
// Match word characters, dots, single quotes, and brackets to support
// identifiers like `ResourceAttributes['service.name']`
const prefix = context.matchBefore(/[\w.'[\]]+/);
// Match word characters, dots, single quotes, brackets, $, {, }, and :
// to support identifiers like `ResourceAttributes['service.name']`,
// macros like `$__dateFilter`, and query params like `{name:Type}`
const prefix = context.matchBefore(/[\w.'[\]${}:]+/);
if (!prefix && !context.explicit) return null;
return {
from: prefix?.from ?? context.pos,
options: completions,
validFor: /^[\w.'[\]]*$/,
validFor: /^[\w.'[\]${}:]*$/,
};
};
}
@ -32,11 +41,13 @@ function createIdentifierCompletionSource(completions: Completion[]) {
export const createCodeMirrorSqlDialect = ({
identifiers,
keywords = ALL_KEYWORDS,
additionalCompletions = [],
includeRegularFunctions = false,
includeAggregateFunctions = false,
}: {
identifiers: string[];
keywords?: string[];
additionalCompletions?: SQLCompletion[];
includeRegularFunctions?: boolean;
includeAggregateFunctions?: boolean;
}) => {
@ -60,6 +71,7 @@ export const createCodeMirrorSqlDialect = ({
apply: `${fn}(`,
}))
: []),
...additionalCompletions,
];
return [

View file

@ -0,0 +1,104 @@
import { replaceMacros } from '../macros';
describe('replaceMacros', () => {
it('should replace $__fromTime with seconds-precision DateTime', () => {
expect(replaceMacros('SELECT $__fromTime')).toBe(
'SELECT toDateTime(fromUnixTimestamp64Milli({startDateMilliseconds:Int64}))',
);
});
it('should replace $__toTime with seconds-precision DateTime', () => {
expect(replaceMacros('SELECT $__toTime')).toBe(
'SELECT toDateTime(fromUnixTimestamp64Milli({endDateMilliseconds:Int64}))',
);
});
it('should replace $__fromTime_ms with millisecond-precision DateTime64', () => {
expect(replaceMacros('SELECT $__fromTime_ms')).toBe(
'SELECT fromUnixTimestamp64Milli({startDateMilliseconds:Int64})',
);
});
it('should replace $__toTime_ms with millisecond-precision DateTime64', () => {
expect(replaceMacros('SELECT $__toTime_ms')).toBe(
'SELECT fromUnixTimestamp64Milli({endDateMilliseconds:Int64})',
);
});
it('should replace $__timeFilter with seconds-precision range filter', () => {
const result = replaceMacros('WHERE $__timeFilter(ts)');
expect(result).toBe(
'WHERE ts >= toDateTime(fromUnixTimestamp64Milli({startDateMilliseconds:Int64})) AND ts <= toDateTime(fromUnixTimestamp64Milli({endDateMilliseconds:Int64}))',
);
});
it('should replace $__timeFilter_ms with millisecond-precision range filter', () => {
const result = replaceMacros('WHERE $__timeFilter_ms(ts)');
expect(result).toBe(
'WHERE ts >= fromUnixTimestamp64Milli({startDateMilliseconds:Int64}) AND ts <= fromUnixTimestamp64Milli({endDateMilliseconds:Int64})',
);
});
it('should replace $__dateFilter with date-only range filter', () => {
const result = replaceMacros('WHERE $__dateFilter(d)');
expect(result).toBe(
'WHERE d >= toDate(fromUnixTimestamp64Milli({startDateMilliseconds:Int64})) AND d <= toDate(fromUnixTimestamp64Milli({endDateMilliseconds:Int64}))',
);
});
it('should replace $__dateTimeFilter with combined date and time filter', () => {
const result = replaceMacros('WHERE $__dateTimeFilter(d, ts)');
expect(result).toBe(
'WHERE (d >= toDate(fromUnixTimestamp64Milli({startDateMilliseconds:Int64})) AND d <= toDate(fromUnixTimestamp64Milli({endDateMilliseconds:Int64}))) AND (ts >= toDateTime(fromUnixTimestamp64Milli({startDateMilliseconds:Int64})) AND ts <= toDateTime(fromUnixTimestamp64Milli({endDateMilliseconds:Int64})))',
);
});
it('should replace $__dt as an alias for dateTimeFilter', () => {
const result = replaceMacros('WHERE $__dt(d, ts)');
expect(result).toBe(
'WHERE (d >= toDate(fromUnixTimestamp64Milli({startDateMilliseconds:Int64})) AND d <= toDate(fromUnixTimestamp64Milli({endDateMilliseconds:Int64}))) AND (ts >= toDateTime(fromUnixTimestamp64Milli({startDateMilliseconds:Int64})) AND ts <= toDateTime(fromUnixTimestamp64Milli({endDateMilliseconds:Int64})))',
);
});
it('should replace $__timeInterval with interval bucketing expression', () => {
const result = replaceMacros('SELECT $__timeInterval(ts)');
expect(result).toBe(
'SELECT toStartOfInterval(toDateTime(ts), INTERVAL {intervalSeconds:Int64} second)',
);
});
it('should replace $__timeInterval_ms with millisecond interval bucketing', () => {
const result = replaceMacros('SELECT $__timeInterval_ms(ts)');
expect(result).toBe(
'SELECT toStartOfInterval(toDateTime64(ts, 3), INTERVAL {intervalMilliseconds:Int64} millisecond)',
);
});
it('should replace $__interval_s with interval seconds param', () => {
expect(replaceMacros('INTERVAL $__interval_s second')).toBe(
'INTERVAL {intervalSeconds:Int64} second',
);
});
it('should replace multiple macros in one query', () => {
const sql =
'SELECT $__timeInterval(ts), count() FROM t WHERE $__timeFilter(ts) GROUP BY 1';
const result = replaceMacros(sql);
expect(result).toContain('toStartOfInterval');
expect(result).toContain(
'ts >= toDateTime(fromUnixTimestamp64Milli({startDateMilliseconds:Int64}))',
);
});
it('should throw on wrong argument count', () => {
expect(() => replaceMacros('$__timeFilter(a, b)')).toThrow(
'expects 1 argument(s), but got 2',
);
});
it('should throw on missing close bracket', () => {
expect(() => replaceMacros('$__timeFilter(col')).toThrow(
'Failed to parse macro arguments',
);
});
});

View file

@ -1,6 +1,6 @@
import { DisplayType } from '@/types';
import { renderRawSqlChartConfig } from '../rawSqlParams';
import { renderRawSqlChartConfig } from '../core/renderChartConfig';
describe('renderRawSqlChartConfig', () => {
describe('DisplayType.Table', () => {

View file

@ -1517,4 +1517,166 @@ describe('renderChartConfig', () => {
endDateMilliseconds: end.getTime(),
});
});
describe('raw sql macro replacement', () => {
const start = new Date('2024-01-01T00:00:00.000Z');
const end = new Date('2024-01-02T00:00:00.000Z');
it('replaces $__dateFilter macro in raw sql config', async () => {
const result = await renderChartConfig(
{
configType: 'sql',
sqlTemplate: 'SELECT * FROM logs WHERE $__dateFilter(d)',
connection: 'conn-1',
dateRange: [start, end],
},
mockMetadata,
undefined,
);
expect(result.sql).toBe(
'SELECT * FROM logs WHERE d >= toDate(fromUnixTimestamp64Milli({startDateMilliseconds:Int64})) AND d <= toDate(fromUnixTimestamp64Milli({endDateMilliseconds:Int64}))',
);
expect(result.params.startDateMilliseconds).toBe(start.getTime());
expect(result.params.endDateMilliseconds).toBe(end.getTime());
});
it('replaces $__timeFilter macro in raw sql config', async () => {
const result = await renderChartConfig(
{
configType: 'sql',
sqlTemplate: 'SELECT * FROM logs WHERE $__timeFilter(ts)',
connection: 'conn-1',
dateRange: [start, end],
},
mockMetadata,
undefined,
);
expect(result.sql).toBe(
'SELECT * FROM logs WHERE ts >= toDateTime(fromUnixTimestamp64Milli({startDateMilliseconds:Int64})) AND ts <= toDateTime(fromUnixTimestamp64Milli({endDateMilliseconds:Int64}))',
);
});
it('replaces $__timeFilter_ms macro in raw sql config', async () => {
const result = await renderChartConfig(
{
configType: 'sql',
sqlTemplate: 'SELECT * FROM logs WHERE $__timeFilter_ms(ts)',
connection: 'conn-1',
dateRange: [start, end],
},
mockMetadata,
undefined,
);
expect(result.sql).toBe(
'SELECT * FROM logs WHERE ts >= fromUnixTimestamp64Milli({startDateMilliseconds:Int64}) AND ts <= fromUnixTimestamp64Milli({endDateMilliseconds:Int64})',
);
});
it('replaces $__fromTime and $__toTime macros in raw sql config', async () => {
const result = await renderChartConfig(
{
configType: 'sql',
sqlTemplate:
'SELECT * FROM logs WHERE ts >= $__fromTime AND ts <= $__toTime',
connection: 'conn-1',
dateRange: [start, end],
},
mockMetadata,
undefined,
);
expect(result.sql).toBe(
'SELECT * FROM logs WHERE ts >= toDateTime(fromUnixTimestamp64Milli({startDateMilliseconds:Int64})) AND ts <= toDateTime(fromUnixTimestamp64Milli({endDateMilliseconds:Int64}))',
);
});
it('replaces $__fromTime_ms and $__toTime_ms macros in raw sql config', async () => {
const result = await renderChartConfig(
{
configType: 'sql',
sqlTemplate:
'SELECT * FROM logs WHERE ts >= $__fromTime_ms AND ts <= $__toTime_ms',
connection: 'conn-1',
dateRange: [start, end],
},
mockMetadata,
undefined,
);
expect(result.sql).toBe(
'SELECT * FROM logs WHERE ts >= fromUnixTimestamp64Milli({startDateMilliseconds:Int64}) AND ts <= fromUnixTimestamp64Milli({endDateMilliseconds:Int64})',
);
});
it('replaces $__dateTimeFilter macro in raw sql config', async () => {
const result = await renderChartConfig(
{
configType: 'sql',
sqlTemplate: 'SELECT * FROM logs WHERE $__dateTimeFilter(d, ts)',
connection: 'conn-1',
dateRange: [start, end],
},
mockMetadata,
undefined,
);
expect(result.sql).toBe(
'SELECT * FROM logs WHERE (d >= toDate(fromUnixTimestamp64Milli({startDateMilliseconds:Int64})) AND d <= toDate(fromUnixTimestamp64Milli({endDateMilliseconds:Int64}))) AND (ts >= toDateTime(fromUnixTimestamp64Milli({startDateMilliseconds:Int64})) AND ts <= toDateTime(fromUnixTimestamp64Milli({endDateMilliseconds:Int64})))',
);
});
it('replaces $__timeInterval macro in raw sql Line config', async () => {
const result = await renderChartConfig(
{
configType: 'sql',
sqlTemplate:
'SELECT $__timeInterval(ts) AS t, count() FROM logs WHERE $__timeFilter(ts) GROUP BY t',
connection: 'conn-1',
displayType: DisplayType.Line,
dateRange: [start, end],
},
mockMetadata,
undefined,
);
expect(result.sql).toContain(
'toStartOfInterval(toDateTime(ts), INTERVAL {intervalSeconds:Int64} second)',
);
expect(result.sql).toContain(
'ts >= toDateTime(fromUnixTimestamp64Milli({startDateMilliseconds:Int64}))',
);
expect(result.params.intervalSeconds).toBeGreaterThan(0);
});
it('replaces $__interval_s macro in raw sql config', async () => {
const result = await renderChartConfig(
{
configType: 'sql',
sqlTemplate:
'SELECT toStartOfInterval(ts, INTERVAL $__interval_s second) FROM logs',
connection: 'conn-1',
displayType: DisplayType.Line,
dateRange: [start, end],
},
mockMetadata,
undefined,
);
expect(result.sql).toBe(
'SELECT toStartOfInterval(ts, INTERVAL {intervalSeconds:Int64} second) FROM logs',
);
expect(result.params.intervalSeconds).toBeGreaterThan(0);
});
it('passes through raw sql with no macros unchanged', async () => {
const sql =
'SELECT count() FROM logs WHERE ts >= {startDateMilliseconds:Int64}';
const result = await renderChartConfig(
{
configType: 'sql',
sqlTemplate: sql,
connection: 'conn-1',
dateRange: [start, end],
},
mockMetadata,
undefined,
);
expect(result.sql).toBe(sql);
});
});
});

View file

@ -16,8 +16,9 @@ import {
splitAndTrimWithBracket,
} from '@/core/utils';
import { isBuilderChartConfig, isRawSqlChartConfig } from '@/guards';
import { replaceMacros } from '@/macros';
import { CustomSchemaSQLSerializerV2, SearchQueryBuilder } from '@/queryParser';
import { renderRawSqlChartConfig } from '@/rawSqlParams';
import { QUERY_PARAMS_BY_DISPLAY_TYPE } from '@/rawSqlParams';
import {
AggregateFunction,
AggregateFunctionWithCombinators,
@ -29,6 +30,7 @@ import {
ChSqlSchema,
CteChartConfig,
DateRange,
DisplayType,
MetricsDataType,
QuerySettings,
RawSqlChartConfig,
@ -1402,6 +1404,24 @@ async function translateMetricChartConfig(
throw new Error(`no query support for metric type=${metricType}`);
}
export function renderRawSqlChartConfig(
chartConfig: RawSqlChartConfig & Partial<DateRange>,
): ChSql {
const displayType = chartConfig.displayType ?? DisplayType.Table;
const sqlWithMacrosReplaced = replaceMacros(chartConfig.sqlTemplate);
// eslint-disable-next-line security/detect-object-injection
const queryParams = QUERY_PARAMS_BY_DISPLAY_TYPE[displayType];
return {
sql: sqlWithMacrosReplaced,
params: Object.fromEntries(
queryParams.map(param => [param.name, param.get(chartConfig)]),
),
};
}
export async function renderChartConfig(
rawChartConfig: ChartConfigWithOptDateRangeEx,
metadata: Metadata,

View file

@ -0,0 +1,201 @@
import { splitAndTrimWithBracket } from './core/utils';
import { renderQueryParam } from './rawSqlParams';
function expectArgs(macroName: string, args: string[], expected: number) {
if (args.length !== expected) {
throw new Error(
`Macro '${macroName}' expects ${expected} argument(s), but got ${args.length}`,
);
}
}
// Helpers to render ClickHouse time conversions using query params
const startMs = () => renderQueryParam('startDateMilliseconds');
const endMs = () => renderQueryParam('endDateMilliseconds');
const intervalS = () => renderQueryParam('intervalSeconds');
const intervalMs = () => renderQueryParam('intervalMilliseconds');
const timeToDate = (msParam: string) =>
`toDate(fromUnixTimestamp64Milli(${msParam}))`;
const timeToDateTime = (msParam: string) =>
`toDateTime(fromUnixTimestamp64Milli(${msParam}))`;
const timeToDateTime64 = (msParam: string) =>
`fromUnixTimestamp64Milli(${msParam})`;
type Macro = {
name: string;
argCount: number;
replace: (args: string[]) => string;
};
const MACROS: Macro[] = [
{
name: 'fromTime',
argCount: 0,
replace: () => timeToDateTime(startMs()),
},
{
name: 'toTime',
argCount: 0,
replace: () => timeToDateTime(endMs()),
},
{
name: 'fromTime_ms',
argCount: 0,
replace: () => timeToDateTime64(startMs()),
},
{
name: 'toTime_ms',
argCount: 0,
replace: () => timeToDateTime64(endMs()),
},
{
name: 'timeFilter',
argCount: 1,
replace: (args: string[]) => {
expectArgs('timeFilter', args, 1);
const [col] = args;
return `${col} >= ${timeToDateTime(startMs())} AND ${col} <= ${timeToDateTime(endMs())}`;
},
},
{
name: 'timeFilter_ms',
argCount: 1,
replace: (args: string[]) => {
expectArgs('timeFilter_ms', args, 1);
const [col] = args;
return `${col} >= ${timeToDateTime64(startMs())} AND ${col} <= ${timeToDateTime64(endMs())}`;
},
},
{
name: 'dateFilter',
argCount: 1,
replace: (args: string[]) => {
expectArgs('dateFilter', args, 1);
const [col] = args;
return `${col} >= ${timeToDate(startMs())} AND ${col} <= ${timeToDate(endMs())}`;
},
},
{
name: 'dateTimeFilter',
argCount: 2,
replace: (args: string[]) => {
expectArgs('dateTimeFilter', args, 2);
const [dateCol, timeCol] = args;
const dateFilter = `(${dateCol} >= ${timeToDate(startMs())} AND ${dateCol} <= ${timeToDate(endMs())})`;
const timeFilter = `(${timeCol} >= ${timeToDateTime(startMs())} AND ${timeCol} <= ${timeToDateTime(endMs())})`;
return `${dateFilter} AND ${timeFilter}`;
},
},
{
name: 'dt',
argCount: 2,
replace: (args: string[]) => {
expectArgs('dt', args, 2);
const [dateCol, timeCol] = args;
const dateFilter = `(${dateCol} >= ${timeToDate(startMs())} AND ${dateCol} <= ${timeToDate(endMs())})`;
const timeFilter = `(${timeCol} >= ${timeToDateTime(startMs())} AND ${timeCol} <= ${timeToDateTime(endMs())})`;
return `${dateFilter} AND ${timeFilter}`;
},
},
{
name: 'timeInterval',
argCount: 1,
replace: (args: string[]) => {
expectArgs('timeInterval', args, 1);
const [col] = args;
return `toStartOfInterval(toDateTime(${col}), INTERVAL ${intervalS()} second)`;
},
},
{
name: 'timeInterval_ms',
argCount: 1,
replace: (args: string[]) => {
expectArgs('timeInterval_ms', args, 1);
const [col] = args;
return `toStartOfInterval(toDateTime64(${col}, 3), INTERVAL ${intervalMs()} millisecond)`;
},
},
{
name: 'interval_s',
argCount: 0,
replace: () => intervalS(),
},
];
/** Macro metadata for autocomplete suggestions */
export const MACRO_SUGGESTIONS = MACROS.map(({ name, argCount }) => ({
name,
argCount,
}));
type MacroMatch = {
full: string;
args: string[];
};
function parseMacroArgs(argString: string): { args: string[]; length: number } {
if (!argString.startsWith('(')) {
return { args: [], length: 0 };
}
// Find the matching close paren
let unmatchedParens = 0;
let closeParenIndex = -1;
for (let i = 0; i < argString.length; i++) {
const c = argString.charAt(i);
if (c === '(') {
unmatchedParens++;
} else if (c === ')') {
unmatchedParens--;
if (unmatchedParens === 0) {
closeParenIndex = i;
break;
}
}
}
if (closeParenIndex < 0) {
return { args: [], length: -1 };
}
const inner = argString.slice(1, closeParenIndex);
const args = splitAndTrimWithBracket(inner);
return { args, length: closeParenIndex + 1 };
}
function findMacros(input: string, name: string): MacroMatch[] {
// eslint-disable-next-line security/detect-non-literal-regexp
const pattern = new RegExp(`\\$__${name}\\b`, 'g');
const matches: MacroMatch[] = [];
for (const match of input.matchAll(pattern)) {
const start = match.index!;
const end = start + match[0].length;
const { args, length } = parseMacroArgs(input.slice(end));
if (length < 0) {
throw new Error('Failed to parse macro arguments');
}
matches.push({ full: input.slice(start, end + length), args });
}
return matches;
}
export function replaceMacros(sql: string): string {
const sortedMacros = [...MACROS].sort(
(m1, m2) => m2.name.length - m1.name.length,
);
for (const macro of sortedMacros) {
const matches = findMacros(sql, macro.name);
for (const match of matches) {
sql = sql.replaceAll(match.full, macro.replace(match.args));
}
}
return sql;
}

View file

@ -110,18 +110,7 @@ export const QUERY_PARAM_EXAMPLES: Record<DisplayType, string> = {
[DisplayType.Markdown]: '',
};
export function renderRawSqlChartConfig(
chartConfig: RawSqlChartConfig & Partial<DateRange>,
): ChSql {
const displayType = chartConfig.displayType ?? DisplayType.Table;
export function renderQueryParam(name: keyof typeof QUERY_PARAMS): string {
// eslint-disable-next-line security/detect-object-injection
const queryParams = QUERY_PARAMS_BY_DISPLAY_TYPE[displayType];
return {
sql: chartConfig.sqlTemplate ?? '',
params: Object.fromEntries(
queryParams.map(param => [param.name, param.get(chartConfig)]),
),
};
return `{${QUERY_PARAMS[name].name}:${QUERY_PARAMS[name].type}}`;
}