From 689e3cd596427a2963886cbff18a68a0c1874814 Mon Sep 17 00:00:00 2001 From: Aaron Knudtson <87577305+knudtty@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:28:53 -0400 Subject: [PATCH 1/5] fix: make filters aware of arrayElement syntax --- packages/app/src/components/DBRowTable.tsx | 17 +++++++++++++ .../src/__tests__/metadata.test.ts | 24 +++++++++++++++++++ packages/common-utils/src/core/metadata.ts | 23 +++++++++++++++--- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 9d2c8c31e6..120d699fe0 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -100,6 +100,7 @@ import { usePrevious, } from '@/utils'; +import { parseMapFieldName } from './DBSearchPageFilters/utils'; import DBRowTableFieldWithPopover from './DBTable/DBRowTableFieldWithPopover'; import DBRowTableRowButtons from './DBTable/DBRowTableRowButtons'; import TableHeader from './DBTable/TableHeader'; @@ -1508,6 +1509,19 @@ function selectColumnMapWithoutAdditionalKeys( ); } +function addMapAliasesToSelect(select: string): string { + const selects = select.split(','); + for (let i = 0; i < selects.length; i++) { + const key = selects[i]; + const mapField = parseMapFieldName(key); + if (mapField) { + selects[i] = + `${key} as "${mapField.baseName}['${mapField.propertyPath}']"`; + } + } + return selects.join(','); +} + export type DBRowTableVariant = 'default' | 'muted'; function DBSqlRowTableComponent({ @@ -1602,6 +1616,9 @@ function DBSqlRowTableComponent({ ...searchChartConfigDefaults(me?.team), ...config, }; + if (typeof base.select === 'string') { + base.select = addMapAliasesToSelect(base.select); + } if (orderByArray.length) { base.orderBy = orderByArray.map(o => { return { diff --git a/packages/common-utils/src/__tests__/metadata.test.ts b/packages/common-utils/src/__tests__/metadata.test.ts index bbd40781ef..083c9005a2 100644 --- a/packages/common-utils/src/__tests__/metadata.test.ts +++ b/packages/common-utils/src/__tests__/metadata.test.ts @@ -1471,4 +1471,28 @@ describe('parseKeyPath', () => { "ResourceAttributes['service.name", ]); }); + + it('parses single-quoted arrayElement function-call form', () => { + expect( + parseKeyPath("arrayElement(ResourceAttributes, 'service.name')"), + ).toEqual(['ResourceAttributes', 'service.name']); + }); + + it('parses arrayElement keys with dots in the map key', () => { + expect( + parseKeyPath("arrayElement(SpanAttributes, 'http.request.method')"), + ).toEqual(['SpanAttributes', 'http.request.method']); + }); + + it('tolerates whitespace inside arrayElement', () => { + expect( + parseKeyPath("arrayElement( ResourceAttributes , 'service.name' )"), + ).toEqual(['ResourceAttributes', 'service.name']); + }); + + it('does not parse arrayElement with a missing closing paren', () => { + expect( + parseKeyPath("arrayElement(ResourceAttributes, 'service.name'"), + ).toEqual(["arrayElement(ResourceAttributes, 'service.name'"]); + }); }); diff --git a/packages/common-utils/src/core/metadata.ts b/packages/common-utils/src/core/metadata.ts index a80feb1fad..59688cef15 100644 --- a/packages/common-utils/src/core/metadata.ts +++ b/packages/common-utils/src/core/metadata.ts @@ -1768,9 +1768,17 @@ export type Field = { }; /** - * Parses a bracket-notation key string into a path array. - * e.g. `ResourceAttributes['service.name']` → `['ResourceAttributes', 'service.name']` - * `ServiceName` → `['ServiceName']` + * Parses a Map-access key string into a path array. + * + * Handles both ClickHouse Map access forms: + * - Bracket form (canonical input): + * `ResourceAttributes['service.name']` → `['ResourceAttributes', 'service.name']` + * `ResourceAttributes["service.name"]` → `['ResourceAttributes', 'service.name']` + * - Function-call form (what ClickHouse renders in result column names): + * `arrayElement(ResourceAttributes, 'service.name')` → `['ResourceAttributes', 'service.name']` + * + * Native columns return a single-element path: + * `ServiceName` → `['ServiceName']` */ export function parseKeyPath(key: string): string[] { const singleIdx = key.indexOf("['"); @@ -1781,6 +1789,15 @@ export function parseKeyPath(key: string): string[] { if (doubleIdx !== -1 && key.endsWith('"]')) { return [key.slice(0, doubleIdx), key.slice(doubleIdx + 2, -2)]; } + // ClickHouse Map access function-call form. ClickHouse renders Map + // subscript expressions (e.g. `SpanAttributes['http.route']`) as + // `arrayElement(SpanAttributes, 'http.route')` in result column names. + const ARRAY_ELEMENT_PREFIX = 'arrayElement('; + if (key.startsWith(ARRAY_ELEMENT_PREFIX) && key.endsWith(')')) { + const inner = key.slice(ARRAY_ELEMENT_PREFIX.length, -1); + const parts = inner.split(',').map(e => e.trim()); + return [parts[0], parts[1].replaceAll("'", '')]; + } return [key]; } From 2a0681879985fdbd6389a4d8fa0d2a3190bbf0b2 Mon Sep 17 00:00:00 2001 From: Aaron Knudtson <87577305+knudtty@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:30:16 -0400 Subject: [PATCH 2/5] add changeset --- .changeset/thin-pets-relate.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/thin-pets-relate.md diff --git a/.changeset/thin-pets-relate.md b/.changeset/thin-pets-relate.md new file mode 100644 index 0000000000..71fe2ca5b1 --- /dev/null +++ b/.changeset/thin-pets-relate.md @@ -0,0 +1,6 @@ +--- +"@hyperdx/common-utils": patch +"@hyperdx/app": patch +--- + +fix: make filters aware of map column return arrayElement From e8dca50848ba83aeb15ac331a01c7112fafb102b Mon Sep 17 00:00:00 2001 From: Aaron Knudtson <87577305+knudtty@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:57:20 -0400 Subject: [PATCH 3/5] fix: use more advanced select statement splitting --- packages/app/src/components/DBRowTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 120d699fe0..2a1028c378 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -1510,7 +1510,7 @@ function selectColumnMapWithoutAdditionalKeys( } function addMapAliasesToSelect(select: string): string { - const selects = select.split(','); + const selects = splitAndTrimWithBracket(select); for (let i = 0; i < selects.length; i++) { const key = selects[i]; const mapField = parseMapFieldName(key); From d920f5f9e0beab1acc9513ae373442b005992146 Mon Sep 17 00:00:00 2001 From: Aaron Knudtson <87577305+knudtty@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:04:43 -0400 Subject: [PATCH 4/5] add testing --- packages/app/src/components/DBRowTable.tsx | 2 +- .../components/__tests__/DBRowTable.test.tsx | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 2a1028c378..0a45ae219f 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -1509,7 +1509,7 @@ function selectColumnMapWithoutAdditionalKeys( ); } -function addMapAliasesToSelect(select: string): string { +export function addMapAliasesToSelect(select: string): string { const selects = splitAndTrimWithBracket(select); for (let i = 0; i < selects.length; i++) { const key = selects[i]; diff --git a/packages/app/src/components/__tests__/DBRowTable.test.tsx b/packages/app/src/components/__tests__/DBRowTable.test.tsx index ffb459b2f5..4f27f58400 100644 --- a/packages/app/src/components/__tests__/DBRowTable.test.tsx +++ b/packages/app/src/components/__tests__/DBRowTable.test.tsx @@ -2,6 +2,7 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { + addMapAliasesToSelect, appendSelectWithAdditionalKeys, RawLogTable, } from '@/components/DBRowTable'; @@ -416,3 +417,45 @@ describe('appendSelectWithAdditionalKeys', () => { }); }); }); + +describe('addMapAliasesToSelect', () => { + it('leaves plain columns unchanged and collapses ", " to ","', () => { + expect(addMapAliasesToSelect('Timestamp, ServiceName, SeverityText')).toBe( + 'Timestamp,ServiceName,SeverityText', + ); + }); + + it('aliases a single-quoted bracket-form map key', () => { + expect(addMapAliasesToSelect("ResourceAttributes['service.name']")).toBe( + `ResourceAttributes['service.name'] as "ResourceAttributes['service.name']"`, + ); + }); + + it('aliases only the map keys in a mixed multi-column input', () => { + expect( + addMapAliasesToSelect( + "Timestamp, ResourceAttributes['service.name'], SeverityText", + ), + ).toBe( + `Timestamp,ResourceAttributes['service.name'] as "ResourceAttributes['service.name']",SeverityText`, + ); + }); + + it('preserves function-call expressions whose arguments contain commas', () => { + expect( + addMapAliasesToSelect( + "Timestamp, toDateTime(now64(3, 'UTC')), ServiceName", + ), + ).toBe(`Timestamp,toDateTime(now64(3, 'UTC')),ServiceName`); + }); + + it('passes already-aliased plain columns through unchanged', () => { + expect(addMapAliasesToSelect('Timestamp as ts, ServiceName as svc')).toBe( + 'Timestamp as ts,ServiceName as svc', + ); + }); + + it('returns an empty string for empty input', () => { + expect(addMapAliasesToSelect('')).toBe(''); + }); +}); From a69bc21a580a2a3af12d591384ed8d469d318289 Mon Sep 17 00:00:00 2001 From: Aaron Knudtson <87577305+knudtty@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:27:35 -0400 Subject: [PATCH 5/5] fix: edge cases --- packages/app/src/components/DBRowTable.tsx | 9 ++-- .../components/__tests__/DBRowTable.test.tsx | 45 +++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 0a45ae219f..53779e8d1d 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -29,6 +29,7 @@ import { isJSDataTypeJSONStringifiable, JSDataType, } from '@hyperdx/common-utils/dist/clickhouse'; +import { parseKeyPath } from '@hyperdx/common-utils/dist/core/metadata'; import { splitAndTrimWithBracket } from '@hyperdx/common-utils/dist/core/utils'; import { BuilderChartConfigWithDateRange, @@ -100,7 +101,6 @@ import { usePrevious, } from '@/utils'; -import { parseMapFieldName } from './DBSearchPageFilters/utils'; import DBRowTableFieldWithPopover from './DBTable/DBRowTableFieldWithPopover'; import DBRowTableRowButtons from './DBTable/DBRowTableRowButtons'; import TableHeader from './DBTable/TableHeader'; @@ -1513,10 +1513,9 @@ export function addMapAliasesToSelect(select: string): string { const selects = splitAndTrimWithBracket(select); for (let i = 0; i < selects.length; i++) { const key = selects[i]; - const mapField = parseMapFieldName(key); - if (mapField) { - selects[i] = - `${key} as "${mapField.baseName}['${mapField.propertyPath}']"`; + const path = parseKeyPath(key); + if (path.length === 2) { + selects[i] = `${key} as "${path[0]}['${path[1]}']"`; } } return selects.join(','); diff --git a/packages/app/src/components/__tests__/DBRowTable.test.tsx b/packages/app/src/components/__tests__/DBRowTable.test.tsx index 4f27f58400..03e8a80db6 100644 --- a/packages/app/src/components/__tests__/DBRowTable.test.tsx +++ b/packages/app/src/components/__tests__/DBRowTable.test.tsx @@ -449,12 +449,57 @@ describe('addMapAliasesToSelect', () => { ).toBe(`Timestamp,toDateTime(now64(3, 'UTC')),ServiceName`); }); + it('aliases an arrayElement(Col, key) Map subscript', () => { + expect( + addMapAliasesToSelect("arrayElement(ResourceAttributes, 'service.name')"), + ).toBe( + `arrayElement(ResourceAttributes, 'service.name') as "ResourceAttributes['service.name']"`, + ); + }); + it('passes already-aliased plain columns through unchanged', () => { expect(addMapAliasesToSelect('Timestamp as ts, ServiceName as svc')).toBe( 'Timestamp as ts,ServiceName as svc', ); }); + it('does NOT re-alias a bracket-form map subscript that already carries an "as " clause', () => { + expect( + addMapAliasesToSelect("ResourceAttributes['service.name'] as svc"), + ).toBe("ResourceAttributes['service.name'] as svc"); + }); + + it('does NOT re-alias an already-aliased map subscript within a multi-column input', () => { + expect( + addMapAliasesToSelect( + "Timestamp, ResourceAttributes['service.name'] as svc, SeverityText", + ), + ).toBe("Timestamp,ResourceAttributes['service.name'] as svc,SeverityText"); + }); + + it('does NOT alias unbracketed dot-notation tokens', () => { + expect(addMapAliasesToSelect('user_data.name')).toBe('user_data.name'); + expect(addMapAliasesToSelect('user_data.address.city')).toBe( + 'user_data.address.city', + ); + }); + + it('does NOT alias multi-segment dotted native columns', () => { + expect(addMapAliasesToSelect('SpanAttributes.k8s.resource.name')).toBe( + 'SpanAttributes.k8s.resource.name', + ); + }); + + it('does NOT alias toString(x).y attribute-access expressions', () => { + expect(addMapAliasesToSelect('toString(SpanAttributes).k8s')).toBe( + 'toString(SpanAttributes).k8s', + ); + }); + + it('does NOT alias backtick-quoted dotted JSON paths', () => { + expect(addMapAliasesToSelect('`host`.`arch`')).toBe('`host`.`arch`'); + }); + it('returns an empty string for empty input', () => { expect(addMapAliasesToSelect('')).toBe(''); });