diff --git a/.changeset/pattern-table-alias-with.md b/.changeset/pattern-table-alias-with.md new file mode 100644 index 0000000000..7d37aa7fef --- /dev/null +++ b/.changeset/pattern-table-alias-with.md @@ -0,0 +1,13 @@ +--- +'@hyperdx/app': patch +--- + +fix(search): keep select-alias filters working in Event Patterns + +Filtering on a column the source exposes only under an alias (for example a +default select of `ServiceName as service`) failed in the Event Patterns view +with `Unknown expression or table expression identifier 'service'`. The +results table works because its own SELECT defines the alias, but Event +Patterns rebuilds the SELECT and did not carry the alias definitions. The +pattern query now receives the same alias `WITH` clauses already threaded into +the results, histogram, and heatmap queries, so the filter resolves. diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index 4cfdda7a29..372aa0e201 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -2193,6 +2193,12 @@ export function DBSearchPage() { config={{ ...chartConfig, dateRange: searchedTimeRange, + // Carry the source's select-alias definitions so the + // rebuilt pattern query can filter on aliased columns + // (e.g. `ServiceName as service`) without hitting + // "Unknown identifier". Mirrors the results, + // histogram, and heatmap configs. + with: aliasWith, }} bodyValueExpression={ searchedSource diff --git a/packages/common-utils/src/__tests__/renderChartConfig.test.ts b/packages/common-utils/src/__tests__/renderChartConfig.test.ts index 35a198d7ea..8b66235c10 100644 --- a/packages/common-utils/src/__tests__/renderChartConfig.test.ts +++ b/packages/common-utils/src/__tests__/renderChartConfig.test.ts @@ -1,6 +1,7 @@ import { chSql, ColumnMeta, parameterizedQueryToSql } from '@/clickhouse'; import { Metadata } from '@/core/metadata'; import { + BuilderChartConfig, ChartConfigWithOptDateRange, DisplayType, MetricsDataType, @@ -552,7 +553,7 @@ describe('renderChartConfig', () => { ); // Two chunked windows of the same chart (most recent window first, - // older windows are end-exclusive — mirrors fetchDataInChunks). + // older windows are end-exclusive, mirroring fetchDataInChunks). const recentChunk = await renderWindow( [new Date('2025-02-12T18:00:00Z'), rankingRange[1]], true, @@ -562,7 +563,7 @@ describe('renderChartConfig', () => { false, ); - // The CTE end is the first `) SELECT` — the outer query starts there. + // The CTE end is the first `) SELECT`; the outer query starts there. const cteOf = (sql: string) => { const start = sql.indexOf('`__hdx_series_limit` AS ('); const end = sql.indexOf(') SELECT '); @@ -667,7 +668,7 @@ describe('renderChartConfig', () => { ), ); expect(sql).toContain('__hdx_series_limit'); - // Each column gets its own NULL check, split on the top-level comma — not + // Each column gets its own NULL check, split on the top-level comma, not // the comma inside Map['...']. expect(sql).toMatch( /LogAttributes\[['"]agentToServer\.capabilities['"]\]\s+IS\s+NOT\s+NULL/, @@ -1215,6 +1216,61 @@ describe('renderChartConfig', () => { }); }); + describe('Event Patterns query with select-alias filter (HDX-1879)', () => { + // The Event Patterns view rebuilds the SELECT (sampled body + timestamp, + // ORDER BY rand() LIMIT) instead of reusing the results-table SELECT. When + // the user filters on a column the source exposes only under an alias + // (e.g. `ServiceName as service`), that alias is out of scope in the + // rebuilt query unless its definition is carried through `with`. Threading + // the source's alias map into the pattern config defines the alias in a + // WITH clause so the filter resolves. + const patternConfig = ( + withClauses: BuilderChartConfig['with'], + ): ChartConfigWithOptDateRange => ({ + connection: 'test-connection', + from: { databaseName: 'default', tableName: 'otel_logs' }, + with: withClauses, + select: 'Body as __hdx_pattern_field, Timestamp as __hdx_timestamp', + where: "service = 'api'", + whereLanguage: 'sql', + orderBy: [{ ordering: 'DESC', valueExpression: 'rand()' }], + limit: { limit: 10000 }, + timestampValueExpression: 'Timestamp', + dateRange: [new Date('2025-01-01'), new Date('2025-01-02')], + }); + + it('defines the select alias in a WITH clause so the filter resolves', async () => { + const generatedSql = await renderChartConfig( + patternConfig([ + { name: 'service', sql: chSql`ServiceName`, isSubquery: false }, + ]), + mockMetadata, + querySettings, + ); + const sql = parameterizedQueryToSql(generatedSql); + + // Alias is defined in the rebuilt pattern query... + expect(sql).toContain('(ServiceName) AS service'); + // ...and the filter still references it. + expect(sql).toContain("service = 'api'"); + }); + + it('omits the alias definition when no alias map is threaded (the bug)', async () => { + const generatedSql = await renderChartConfig( + patternConfig(undefined), + mockMetadata, + querySettings, + ); + const sql = parameterizedQueryToSql(generatedSql); + + // Without the threaded WITH clauses the alias is undefined, so the + // filter references a `service` column that does not exist in the + // rebuilt SELECT (ClickHouse rejects this with "Unknown identifier"). + expect(sql).not.toContain('AS service'); + expect(sql).toContain("service = 'api'"); + }); + }); + describe('SQL filter KV items direct_read optimization', () => { const stubKvItemsMetadata = () => { mockMetadata.getColumns = jest.fn().mockResolvedValue([ @@ -3226,7 +3282,7 @@ describe('renderChartConfig', () => { undefined, ); - // PromQL configs return empty SQL — queries go through the Prometheus API route + // PromQL configs return empty SQL; queries go through the Prometheus API route expect(generatedSql.sql).toBe(''); expect(generatedSql.params).toEqual({}); });