From 98ecc1ff4dbf6b2da325e8b2cb0868a5f9b0bb57 Mon Sep 17 00:00:00 2001 From: CG-5228 Date: Mon, 26 Oct 2026 22:11:49 +0000 Subject: [PATCH 1/7] fix(common-utils): split comma-separated string groupBy in increase ranking CTE --- .../src/__tests__/renderChartConfig.test.ts | 53 +++++++++++++++++++ .../src/core/renderChartConfig.ts | 38 ++++++++----- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/packages/common-utils/src/__tests__/renderChartConfig.test.ts b/packages/common-utils/src/__tests__/renderChartConfig.test.ts index 41aea39ed9..f071e0d052 100644 --- a/packages/common-utils/src/__tests__/renderChartConfig.test.ts +++ b/packages/common-utils/src/__tests__/renderChartConfig.test.ts @@ -577,6 +577,59 @@ describe('renderChartConfig', () => { ); }); + it('should handle aggFn=increase with comma-separated string groupBy', async () => { + const config: ChartConfigWithOptDateRange = { + displayType: DisplayType.Line, + connection: 'test-connection', + metricTables: { + gauge: 'otel_metrics_gauge', + histogram: 'otel_metrics_histogram', + sum: 'otel_metrics_sum', + summary: 'otel_metrics_summary', + 'exponential histogram': 'otel_metrics_exponential_histogram', + }, + from: { + databaseName: 'default', + tableName: '', + }, + select: [ + { + aggFn: 'increase', + aggCondition: '', + aggConditionLanguage: 'lucene', + valueExpression: 'Value', + metricName: 'istio_requests_total', + metricType: MetricsDataType.Sum, + }, + ], + groupBy: + "Attributes['destination_service_name'], Attributes['response_code']", + where: '', + whereLanguage: 'sql', + timestampValueExpression: 'TimeUnix', + dateRange: [new Date('2025-02-12'), new Date('2025-12-14')], + granularity: '5 minute', + limit: { limit: 100000 }, + }; + + const generatedSql = await renderChartConfig( + config, + mockMetadata, + querySettings, + ); + const actual = parameterizedQueryToSql(generatedSql); + expect(actual).toMatch( + /tuple\(\s*Attributes\['destination_service_name'\]\s*,\s*Attributes\['response_code'\]\s*\)/, + ); + expect(actual).toContain('TopGroups'); + expect(actual).toMatch( + /Attributes\['destination_service_name'\]\s+IS\s+NOT\s+NULL\s+AND\s+toString\(Attributes\['destination_service_name'\]\)\s*!=\s*''/, + ); + expect(actual).toMatch( + /Attributes\['response_code'\]\s+IS\s+NOT\s+NULL\s+AND\s+toString\(Attributes\['response_code'\]\)\s*!=\s*''/, + ); + }); + it('should not emit a rank CTE when aggFn=increase has no groupBy', async () => { const config: ChartConfigWithOptDateRange = { displayType: DisplayType.Line, diff --git a/packages/common-utils/src/core/renderChartConfig.ts b/packages/common-utils/src/core/renderChartConfig.ts index 8de7551337..a68b2ea478 100644 --- a/packages/common-utils/src/core/renderChartConfig.ts +++ b/packages/common-utils/src/core/renderChartConfig.ts @@ -2338,25 +2338,35 @@ async function translateMetricChartConfig( // these expressions go inside tuple(...) in the ranking CTE and the // outer IN predicate, where an `AS "alias"` suffix is a syntax error // (mirrors the strip in renderSeriesLimitCte). - const groupByForRank = await renderSelectList( - typeof chartConfig.groupBy === 'string' - ? chartConfig.groupBy - : chartConfig.groupBy!.map(col => ({ ...col, alias: undefined })), - { - ...chartConfig, - from: { databaseName: '', tableName: 'Bucketed' }, - with: sumWith, - } as BuilderChartConfigWithOptDateRangeEx, - metadata, - { mergeRatio: false }, - ); - const groupBySql = concatChSql(',', groupByForRank); + // One ChSql per group-by column (groupBy may be an array or a + // comma-separated string). splitAndTrimWithBracket respects []/()/quotes + // so it won't split inside Map['a,b']; the per-column null filter below + // needs them separated. + let groupByCols: ChSql[]; + if (typeof chartConfig.groupBy === 'string') { + groupByCols = splitAndTrimWithBracket(chartConfig.groupBy).map( + col => chSql`${{ UNSAFE_RAW_SQL: col }}`, + ); + } else { + const rendered = await renderSelectList( + chartConfig.groupBy!.map(col => ({ ...col, alias: undefined })), + { + ...chartConfig, + from: { databaseName: '', tableName: 'Bucketed' }, + with: sumWith, + } as BuilderChartConfigWithOptDateRangeEx, + metadata, + { mergeRatio: false }, + ); + groupByCols = Array.isArray(rendered) ? rendered : [rendered]; + } + const groupBySql = concatChSql(',', groupByCols); // Exclude rows where any groupBy column is NULL/empty so they don't // collapse into a single dominating '-' series. const groupByEmptyFilter = concatChSql( ' AND ', - (Array.isArray(groupByForRank) ? groupByForRank : [groupByForRank]).map( + groupByCols.map( g => chSql`(${g} IS NOT NULL AND toString(${g}) != '')`, ), ); From 32dc19005b877a582b7694a84910cfcb12783ef0 Mon Sep 17 00:00:00 2001 From: CG-5228 Date: Tue, 27 Oct 2026 11:49:50 +0000 Subject: [PATCH 2/7] refactor(common-utils): extract shared renderGroupByColumns helper --- .../src/__tests__/renderChartConfig.test.ts | 6 +- .../src/core/renderChartConfig.ts | 84 +++++++++---------- 2 files changed, 42 insertions(+), 48 deletions(-) diff --git a/packages/common-utils/src/__tests__/renderChartConfig.test.ts b/packages/common-utils/src/__tests__/renderChartConfig.test.ts index f071e0d052..ae29f463e6 100644 --- a/packages/common-utils/src/__tests__/renderChartConfig.test.ts +++ b/packages/common-utils/src/__tests__/renderChartConfig.test.ts @@ -603,7 +603,7 @@ describe('renderChartConfig', () => { }, ], groupBy: - "Attributes['destination_service_name'], Attributes['response_code']", + "Attributes['destination_service_name'], Attributes['a,b']", where: '', whereLanguage: 'sql', timestampValueExpression: 'TimeUnix', @@ -619,14 +619,14 @@ describe('renderChartConfig', () => { ); const actual = parameterizedQueryToSql(generatedSql); expect(actual).toMatch( - /tuple\(\s*Attributes\['destination_service_name'\]\s*,\s*Attributes\['response_code'\]\s*\)/, + /tuple\(\s*Attributes\['destination_service_name'\]\s*,\s*Attributes\['a,b'\]\s*\)/, ); expect(actual).toContain('TopGroups'); expect(actual).toMatch( /Attributes\['destination_service_name'\]\s+IS\s+NOT\s+NULL\s+AND\s+toString\(Attributes\['destination_service_name'\]\)\s*!=\s*''/, ); expect(actual).toMatch( - /Attributes\['response_code'\]\s+IS\s+NOT\s+NULL\s+AND\s+toString\(Attributes\['response_code'\]\)\s*!=\s*''/, + /Attributes\['a,b'\]\s+IS\s+NOT\s+NULL\s+AND\s+toString\(Attributes\['a,b'\]\)\s*!=\s*''/, ); }); diff --git a/packages/common-utils/src/core/renderChartConfig.ts b/packages/common-utils/src/core/renderChartConfig.ts index a68b2ea478..7f9bcdf621 100644 --- a/packages/common-utils/src/core/renderChartConfig.ts +++ b/packages/common-utils/src/core/renderChartConfig.ts @@ -1554,6 +1554,30 @@ async function renderGroupBy( ); } +async function renderGroupByColumns( + groupBy: NonNullable, + config: BuilderChartConfigWithOptDateRangeEx, + metadata: Metadata, +): Promise { + // One ChSql per group-by column (groupBy may be an array or a comma-separated + // string). splitAndTrimWithBracket respects []/()/quotes so it won't split + // inside Map['a,b']; per-column null filters need them separated. + if (typeof groupBy === 'string') { + return splitAndTrimWithBracket(groupBy).map( + col => chSql`${{ UNSAFE_RAW_SQL: col }}`, + ); + } + // Strip aliases: these go inside tuple(...)/`IS NOT NULL`, where an + // `AS "alias"` suffix is a syntax error (unlike the outer GROUP BY). + const rendered = await renderSelectList( + groupBy.map(col => ({ ...col, alias: undefined })), + config, + metadata, + { mergeRatio: false }, + ); + return Array.isArray(rendered) ? rendered : [rendered]; +} + async function renderSeriesLimitCte( chartConfig: BuilderChartConfigWithOptDateRangeEx, metadata: Metadata, @@ -1603,25 +1627,11 @@ async function renderSeriesLimitCte( ]) : []; - // One ChSql per group-by column (groupBy may be an array or a comma-separated - // string). splitAndTrimWithBracket respects []/()/quotes so it won't split - // inside Map['a,b']; the per-column null filter below needs them separated. - let groupByCols: ChSql[]; - if (typeof chartConfig.groupBy === 'string') { - groupByCols = splitAndTrimWithBracket(chartConfig.groupBy).map( - col => chSql`${{ UNSAFE_RAW_SQL: col }}`, - ); - } else { - // Strip aliases: these go inside tuple(...)/`IS NOT NULL`, where an - // `AS "alias"` suffix is a syntax error (unlike the outer GROUP BY). - const rendered = await renderSelectList( - chartConfig.groupBy.map(col => ({ ...col, alias: undefined })), - chartConfig, - metadata, - { mergeRatio: false }, - ); - groupByCols = Array.isArray(rendered) ? rendered : [rendered]; - } + const groupByCols = await renderGroupByColumns( + chartConfig.groupBy!, + chartConfig, + metadata, + ); const groupByTuple = concatChSql(',', groupByCols); // Rank by the value the chart actually plots, so "top N" means top N by what @@ -2334,32 +2344,16 @@ async function translateMetricChartConfig( if (shouldApplyIncreaseGroupLimit) { // Render the user's groupBy against the Bucketed CTE so column - // references resolve to the CTE's projection. Aliases are stripped: - // these expressions go inside tuple(...) in the ranking CTE and the - // outer IN predicate, where an `AS "alias"` suffix is a syntax error - // (mirrors the strip in renderSeriesLimitCte). - // One ChSql per group-by column (groupBy may be an array or a - // comma-separated string). splitAndTrimWithBracket respects []/()/quotes - // so it won't split inside Map['a,b']; the per-column null filter below - // needs them separated. - let groupByCols: ChSql[]; - if (typeof chartConfig.groupBy === 'string') { - groupByCols = splitAndTrimWithBracket(chartConfig.groupBy).map( - col => chSql`${{ UNSAFE_RAW_SQL: col }}`, - ); - } else { - const rendered = await renderSelectList( - chartConfig.groupBy!.map(col => ({ ...col, alias: undefined })), - { - ...chartConfig, - from: { databaseName: '', tableName: 'Bucketed' }, - with: sumWith, - } as BuilderChartConfigWithOptDateRangeEx, - metadata, - { mergeRatio: false }, - ); - groupByCols = Array.isArray(rendered) ? rendered : [rendered]; - } + // references resolve to the CTE's projection. + const groupByCols = await renderGroupByColumns( + chartConfig.groupBy!, + { + ...chartConfig, + from: { databaseName: '', tableName: 'Bucketed' }, + with: sumWith, + } as BuilderChartConfigWithOptDateRangeEx, + metadata, + ); const groupBySql = concatChSql(',', groupByCols); // Exclude rows where any groupBy column is NULL/empty so they don't From 79e0aa2137e59e9adb69396edf74ab43bd6189b9 Mon Sep 17 00:00:00 2001 From: CG-5228 Date: Wed, 28 Oct 2026 20:51:18 +0000 Subject: [PATCH 3/7] chore(common-utils): add changeset and clarify renderGroupByColumns comments --- .changeset/full-yaks-mate.md | 5 +++++ packages/common-utils/src/core/renderChartConfig.ts | 10 ++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .changeset/full-yaks-mate.md diff --git a/.changeset/full-yaks-mate.md b/.changeset/full-yaks-mate.md new file mode 100644 index 0000000000..8375563960 --- /dev/null +++ b/.changeset/full-yaks-mate.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/common-utils": patch +--- + +fix(common-utils): split comma-separated string groupBy in increase ranking CTE diff --git a/packages/common-utils/src/core/renderChartConfig.ts b/packages/common-utils/src/core/renderChartConfig.ts index 7f9bcdf621..bfa9185596 100644 --- a/packages/common-utils/src/core/renderChartConfig.ts +++ b/packages/common-utils/src/core/renderChartConfig.ts @@ -1560,8 +1560,10 @@ async function renderGroupByColumns( metadata: Metadata, ): Promise { // One ChSql per group-by column (groupBy may be an array or a comma-separated - // string). splitAndTrimWithBracket respects []/()/quotes so it won't split - // inside Map['a,b']; per-column null filters need them separated. + // string). The string branch emits raw SQL per split column and does not + // consult `config` or `metadata`; only the array branch resolves columns via + // `renderSelectList(config, metadata)`. splitAndTrimWithBracket respects + // []/()/quotes so it won't split inside Map['a,b']. if (typeof groupBy === 'string') { return splitAndTrimWithBracket(groupBy).map( col => chSql`${{ UNSAFE_RAW_SQL: col }}`, @@ -2343,8 +2345,8 @@ async function translateMetricChartConfig( let outerWhere: string = ''; if (shouldApplyIncreaseGroupLimit) { - // Render the user's groupBy against the Bucketed CTE so column - // references resolve to the CTE's projection. + // For array groupBy, resolve column references against the Bucketed CTE + // projection; string groupBy is split directly into per-column SQL. const groupByCols = await renderGroupByColumns( chartConfig.groupBy!, { From d347903ddb8080e244d36e69d12f4c90766d90cf Mon Sep 17 00:00:00 2001 From: CG-5228 Date: Thu, 29 Oct 2026 15:56:48 +0000 Subject: [PATCH 4/7] chore(common-utils): drop redundant non-null assertion and assert single-arg toString --- packages/common-utils/src/__tests__/renderChartConfig.test.ts | 1 + packages/common-utils/src/core/renderChartConfig.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/common-utils/src/__tests__/renderChartConfig.test.ts b/packages/common-utils/src/__tests__/renderChartConfig.test.ts index ae29f463e6..adf4851b74 100644 --- a/packages/common-utils/src/__tests__/renderChartConfig.test.ts +++ b/packages/common-utils/src/__tests__/renderChartConfig.test.ts @@ -575,6 +575,7 @@ describe('renderChartConfig', () => { expect(actual).toMatch( /Attributes\[['"]env['"]\]\s+IS\s+NOT\s+NULL\s+AND\s+toString\(Attributes\[['"]env['"]\]\)\s*!=\s*''/, ); + expect(actual).not.toMatch(/toString\([^)]*,/); }); it('should handle aggFn=increase with comma-separated string groupBy', async () => { diff --git a/packages/common-utils/src/core/renderChartConfig.ts b/packages/common-utils/src/core/renderChartConfig.ts index bfa9185596..88eb77d570 100644 --- a/packages/common-utils/src/core/renderChartConfig.ts +++ b/packages/common-utils/src/core/renderChartConfig.ts @@ -1533,6 +1533,7 @@ async function renderGroupBy( chartConfig: BuilderChartConfigWithOptDateRangeEx, metadata: Metadata, ): Promise { + // Note: per-column splitting for string groupBy happens in renderGroupByColumns. return concatChSql( ',', isUsingGroupBy(chartConfig) @@ -1630,7 +1631,7 @@ async function renderSeriesLimitCte( : []; const groupByCols = await renderGroupByColumns( - chartConfig.groupBy!, + chartConfig.groupBy, chartConfig, metadata, ); From a42059a5fb28fedf172c8f5778277780dc401a56 Mon Sep 17 00:00:00 2001 From: CG-5228 Date: Fri, 30 Oct 2026 21:41:03 +0000 Subject: [PATCH 5/7] chore(common-utils): clarify renderGroupBy comment and align chartConfig param name --- .../common-utils/src/core/renderChartConfig.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/common-utils/src/core/renderChartConfig.ts b/packages/common-utils/src/core/renderChartConfig.ts index 88eb77d570..821054f425 100644 --- a/packages/common-utils/src/core/renderChartConfig.ts +++ b/packages/common-utils/src/core/renderChartConfig.ts @@ -1533,7 +1533,9 @@ async function renderGroupBy( chartConfig: BuilderChartConfigWithOptDateRangeEx, metadata: Metadata, ): Promise { - // Note: per-column splitting for string groupBy happens in renderGroupByColumns. + // The outer GROUP BY keeps a comma-separated string groupBy intact as a + // single expression; per-column splitting for tuple(...)/IN contexts is + // handled separately by renderGroupByColumns. return concatChSql( ',', isUsingGroupBy(chartConfig) @@ -1557,14 +1559,14 @@ async function renderGroupBy( async function renderGroupByColumns( groupBy: NonNullable, - config: BuilderChartConfigWithOptDateRangeEx, + chartConfig: BuilderChartConfigWithOptDateRangeEx, metadata: Metadata, ): Promise { // One ChSql per group-by column (groupBy may be an array or a comma-separated // string). The string branch emits raw SQL per split column and does not - // consult `config` or `metadata`; only the array branch resolves columns via - // `renderSelectList(config, metadata)`. splitAndTrimWithBracket respects - // []/()/quotes so it won't split inside Map['a,b']. + // consult `chartConfig` or `metadata`; only the array branch resolves columns + // via `renderSelectList(chartConfig, metadata)`. splitAndTrimWithBracket + // respects []/()/quotes so it won't split inside Map['a,b']. if (typeof groupBy === 'string') { return splitAndTrimWithBracket(groupBy).map( col => chSql`${{ UNSAFE_RAW_SQL: col }}`, @@ -1574,7 +1576,7 @@ async function renderGroupByColumns( // `AS "alias"` suffix is a syntax error (unlike the outer GROUP BY). const rendered = await renderSelectList( groupBy.map(col => ({ ...col, alias: undefined })), - config, + chartConfig, metadata, { mergeRatio: false }, ); From 9602ca0bfdfada3cb2fdfa00ff0bee644554a6a1 Mon Sep 17 00:00:00 2001 From: CG-5228 Date: Sat, 31 Oct 2026 01:58:23 +0000 Subject: [PATCH 6/7] chore(common-utils): guard empty groupByCols and tighten increase string groupBy assertions --- .../src/__tests__/renderChartConfig.test.ts | 4 +- .../src/core/renderChartConfig.ts | 54 ++++++++++--------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/packages/common-utils/src/__tests__/renderChartConfig.test.ts b/packages/common-utils/src/__tests__/renderChartConfig.test.ts index adf4851b74..9bd31ca353 100644 --- a/packages/common-utils/src/__tests__/renderChartConfig.test.ts +++ b/packages/common-utils/src/__tests__/renderChartConfig.test.ts @@ -620,15 +620,15 @@ describe('renderChartConfig', () => { ); const actual = parameterizedQueryToSql(generatedSql); expect(actual).toMatch( - /tuple\(\s*Attributes\['destination_service_name'\]\s*,\s*Attributes\['a,b'\]\s*\)/, + /tuple\(\s*Attributes\['destination_service_name'\]\s*,\s*Attributes\['a,b'\]\s*\)\s+IN\s+\(SELECT\s+`group`\s+FROM\s+TopGroups\)/, ); - expect(actual).toContain('TopGroups'); expect(actual).toMatch( /Attributes\['destination_service_name'\]\s+IS\s+NOT\s+NULL\s+AND\s+toString\(Attributes\['destination_service_name'\]\)\s*!=\s*''/, ); expect(actual).toMatch( /Attributes\['a,b'\]\s+IS\s+NOT\s+NULL\s+AND\s+toString\(Attributes\['a,b'\]\)\s*!=\s*''/, ); + expect(actual).not.toMatch(/toString\([^)]*\],\s*Attributes\[/); }); it('should not emit a rank CTE when aggFn=increase has no groupBy', async () => { diff --git a/packages/common-utils/src/core/renderChartConfig.ts b/packages/common-utils/src/core/renderChartConfig.ts index 821054f425..b1eb8faec3 100644 --- a/packages/common-utils/src/core/renderChartConfig.ts +++ b/packages/common-utils/src/core/renderChartConfig.ts @@ -2359,23 +2359,24 @@ async function translateMetricChartConfig( } as BuilderChartConfigWithOptDateRangeEx, metadata, ); - const groupBySql = concatChSql(',', groupByCols); - - // Exclude rows where any groupBy column is NULL/empty so they don't - // collapse into a single dominating '-' series. - const groupByEmptyFilter = concatChSql( - ' AND ', - groupByCols.map( - g => chSql`(${g} IS NOT NULL AND toString(${g}) != '')`, - ), - ); + if (groupByCols.length > 0) { + const groupBySql = concatChSql(',', groupByCols); + + // Exclude rows where any groupBy column is NULL/empty so they don't + // collapse into a single dominating '-' series. + const groupByEmptyFilter = concatChSql( + ' AND ', + groupByCols.map( + g => chSql`(${g} IS NOT NULL AND toString(${g}) != '')`, + ), + ); - // Rank by max-per-bucket summed Rate so a group that spikes in one - // bucket still makes the top N. tuple() wraps multi-column groupBys - // into a single comparable column. - sumWith.push({ - name: 'TopGroups', - sql: chSql` + // Rank by max-per-bucket summed Rate so a group that spikes in one + // bucket still makes the top N. tuple() wraps multi-column groupBys + // into a single comparable column. + sumWith.push({ + name: 'TopGroups', + sql: chSql` SELECT \`group\` FROM ( SELECT @@ -2389,18 +2390,19 @@ async function translateMetricChartConfig( ORDER BY max(\`bucket_value\`) DESC, \`group\` LIMIT ${{ Int32: INCREASE_MAX_NUM_GROUPS }} `, - }); + }); - // Safety: groupBySql is built from metric groupBy expressions which are - // always simple column references (UNSAFE_RAW_SQL). Verify no parameterized - // values leaked through — if they did, .sql would contain param placeholders - // but the string-based outer WHERE would lose the param bindings. - if (Object.keys(groupBySql.params).length > 0) { - throw new Error( - 'increase + groupBy: unexpected parameterized groupBy expressions', - ); + // Safety: groupBySql is built from metric groupBy expressions which are + // always simple column references (UNSAFE_RAW_SQL). Verify no parameterized + // values leaked through — if they did, .sql would contain param placeholders + // but the string-based outer WHERE would lose the param bindings. + if (Object.keys(groupBySql.params).length > 0) { + throw new Error( + 'increase + groupBy: unexpected parameterized groupBy expressions', + ); + } + outerWhere = `tuple(${groupBySql.sql}) IN (SELECT \`group\` FROM TopGroups)`; } - outerWhere = `tuple(${groupBySql.sql}) IN (SELECT \`group\` FROM TopGroups)`; } return { From f3270d3183133f0adc7a9377bf576ef7c3f95962 Mon Sep 17 00:00:00 2001 From: CG-5228 Date: Wed, 4 Nov 2026 10:59:51 +0000 Subject: [PATCH 7/7] test(common-utils): cover aggFn=increase with whitespace/comma-only string groupBy --- .../src/__tests__/renderChartConfig.test.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/common-utils/src/__tests__/renderChartConfig.test.ts b/packages/common-utils/src/__tests__/renderChartConfig.test.ts index 66255c32aa..1408ca142d 100644 --- a/packages/common-utils/src/__tests__/renderChartConfig.test.ts +++ b/packages/common-utils/src/__tests__/renderChartConfig.test.ts @@ -673,6 +673,50 @@ describe('renderChartConfig', () => { expect(actual).not.toContain('TopGroups'); }); + it('should not emit a rank CTE when aggFn=increase has a whitespace/comma-only string groupBy', async () => { + const config: ChartConfigWithOptDateRange = { + displayType: DisplayType.Line, + connection: 'test-connection', + metricTables: { + gauge: 'otel_metrics_gauge', + histogram: 'otel_metrics_histogram', + sum: 'otel_metrics_sum', + summary: 'otel_metrics_summary', + 'exponential histogram': 'otel_metrics_exponential_histogram', + }, + from: { + databaseName: 'default', + tableName: '', + }, + select: [ + { + aggFn: 'increase', + aggCondition: '', + aggConditionLanguage: 'lucene', + valueExpression: 'Value', + metricName: 'db.client.connections.usage', + metricType: MetricsDataType.Sum, + }, + ], + groupBy: ' , ', + where: '', + whereLanguage: 'sql', + timestampValueExpression: 'TimeUnix', + dateRange: [new Date('2025-02-12'), new Date('2025-12-14')], + granularity: '5 minute', + limit: { limit: 10 }, + }; + + const generatedSql = await renderChartConfig( + config, + mockMetadata, + querySettings, + ); + const actual = parameterizedQueryToSql(generatedSql); + expect(actual).not.toContain('TopGroups'); + expect(actual).not.toContain('IN (SELECT `group`'); + }); + it('renders bound values (not template macros) when generateSqlTemplate is unset', async () => { // Regression guard for the generateSqlTemplate flag threaded through // timeFilterExpr / timeBucketExpr / renderFrom / renderWhere: real query