Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/pattern-table-alias-with.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions packages/app/src/DBSearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 60 additions & 4 deletions packages/common-utils/src/__tests__/renderChartConfig.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { chSql, ColumnMeta, parameterizedQueryToSql } from '@/clickhouse';
import { Metadata } from '@/core/metadata';
import {
BuilderChartConfig,
ChartConfigWithOptDateRange,
DisplayType,
MetricsDataType,
Expand Down Expand Up @@ -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,
Expand All @@ -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 ');
Expand Down Expand Up @@ -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/,
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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({});
});
Expand Down
Loading