Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/thin-pets-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hyperdx/common-utils": patch
"@hyperdx/app": patch
---

fix: make filters aware of map column return arrayElement
16 changes: 16 additions & 0 deletions packages/app/src/components/DBRowTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1508,6 +1509,18 @@ function selectColumnMapWithoutAdditionalKeys(
);
}

export function addMapAliasesToSelect(select: string): string {
const selects = splitAndTrimWithBracket(select);
for (let i = 0; i < selects.length; i++) {
const key = selects[i];
const path = parseKeyPath(key);
if (path.length === 2) {
selects[i] = `${key} as "${path[0]}['${path[1]}']"`;
}
}
return selects.join(',');
}

export type DBRowTableVariant = 'default' | 'muted';

function DBSqlRowTableComponent({
Expand Down Expand Up @@ -1602,6 +1615,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 {
Expand Down
88 changes: 88 additions & 0 deletions packages/app/src/components/__tests__/DBRowTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import {
addMapAliasesToSelect,
appendSelectWithAdditionalKeys,
RawLogTable,
} from '@/components/DBRowTable';
Expand Down Expand Up @@ -416,3 +417,90 @@ 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('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 <alias>" 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('');
});
});
24 changes: 24 additions & 0 deletions packages/common-utils/src/__tests__/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'"]);
});
});
23 changes: 20 additions & 3 deletions packages/common-utils/src/core/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("['");
Expand All @@ -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];
}

Expand Down
Loading