Skip to content

Commit 52483f6

Browse files
authored
feat: enable filters for json columns (#1087)
Closes HDX-1965
1 parent 784014b commit 52483f6

File tree

5 files changed

+131
-20
lines changed

5 files changed

+131
-20
lines changed

.changeset/nervous-guests-help.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@hyperdx/common-utils": patch
3+
"@hyperdx/app": patch
4+
---
5+
6+
feat: enable filters for json columns

packages/app/src/components/DBRowJsonViewer.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -154,20 +154,10 @@ export function DBRowJsonViewer({
154154
const getLineActions = useCallback<GetLineActions>(
155155
({ keyPath, value }) => {
156156
const actions: LineAction[] = [];
157-
let fieldPath = mergePath(keyPath);
157+
const fieldPath = mergePath(keyPath, jsonColumns);
158158
const isJsonColumn =
159159
keyPath.length > 0 && jsonColumns?.includes(keyPath[0]);
160160

161-
if (isJsonColumn) {
162-
fieldPath = keyPath.join('.');
163-
if (keyPath.length > 1) {
164-
fieldPath = `${keyPath[0]}.${keyPath
165-
.slice(1)
166-
.map(k => `\`${k}\``)
167-
.join('.')}`;
168-
}
169-
}
170-
171161
// Add to Filters action (strings only)
172162
// FIXME: TOTAL HACK To disallow adding timestamp to filters
173163
if (

packages/app/src/components/DBSearchPageFilters.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@ import {
1717
Tooltip,
1818
UnstyledButton,
1919
} from '@mantine/core';
20+
import { notifications } from '@mantine/notifications';
2021
import { IconSearch } from '@tabler/icons-react';
2122

2223
import { useExplainQuery } from '@/hooks/useExplainQuery';
23-
import { useAllFields, useGetKeyValues } from '@/hooks/useMetadata';
24+
import {
25+
useAllFields,
26+
useGetKeyValues,
27+
useJsonColumns,
28+
} from '@/hooks/useMetadata';
2429
import useResizable from '@/hooks/useResizable';
2530
import { getMetadata } from '@/metadata';
2631
import { FilterStateHook, usePinnedFilters } from '@/searchFilters';
@@ -406,11 +411,26 @@ const DBSearchPageFiltersComponent = ({
406411
const { data: countData } = useExplainQuery(chartConfig);
407412
const numRows: number = countData?.[0]?.rows ?? 0;
408413

409-
const { data, isLoading } = useAllFields({
414+
const { data: jsonColumns } = useJsonColumns({
415+
databaseName: chartConfig.from.databaseName,
416+
tableName: chartConfig.from.tableName,
417+
connectionId: chartConfig.connection,
418+
});
419+
const { data, isLoading, error } = useAllFields({
410420
databaseName: chartConfig.from.databaseName,
411421
tableName: chartConfig.from.tableName,
412422
connectionId: chartConfig.connection,
413423
});
424+
useEffect(() => {
425+
if (error) {
426+
notifications.show({
427+
color: 'red',
428+
title: error?.name,
429+
message: error?.message,
430+
autoClose: 5000,
431+
});
432+
}
433+
}, [error]);
414434

415435
const [showMoreFields, setShowMoreFields] = useState(false);
416436

@@ -431,7 +451,7 @@ const DBSearchPageFiltersComponent = ({
431451
// todo: add number type with sliders :D
432452
)
433453
.map(({ path, type }) => {
434-
return { type, path: mergePath(path) };
454+
return { type, path: mergePath(path, jsonColumns ?? []) };
435455
})
436456
.filter(
437457
field =>
@@ -445,9 +465,8 @@ const DBSearchPageFiltersComponent = ({
445465
path =>
446466
!['body', 'timestamp', '_hdx_body'].includes(path.toLowerCase()),
447467
);
448-
449468
return strings;
450-
}, [data, filterState, showMoreFields]);
469+
}, [data, jsonColumns, filterState, showMoreFields]);
451470

452471
// Special case for live tail
453472
const [dateRange, setDateRange] = useState<[Date, Date]>(

packages/app/src/utils.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -646,12 +646,21 @@ export const legacyMetricNameToNameAndDataType = (metricName?: string) => {
646646
};
647647

648648
// Date formatting
649-
export const mergePath = (path: string[]) => {
649+
export const mergePath = (path: string[], jsonColumns: string[] = []) => {
650650
const [key, ...rest] = path;
651651
if (rest.length === 0) {
652652
return key;
653653
}
654-
return `${key}['${rest.join("']['")}']`;
654+
return jsonColumns.includes(key)
655+
? `${key}.${rest
656+
.map(v =>
657+
v
658+
.split('.')
659+
.map(v => (v.startsWith('`') && v.endsWith('`') ? v : `\`${v}\``))
660+
.join('.'),
661+
)
662+
.join('.')}`
663+
: `${key}['${rest.join("']['")}']`;
655664
};
656665

657666
export const _useTry = <T>(fn: () => T): [null | Error | unknown, null | T] => {

packages/common-utils/src/metadata.ts

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type { ChartConfig, ChartConfigWithDateRange, TSource } from '@/types';
1616
// If filters initially are taking too long to load, decrease this number.
1717
// Between 1e6 - 5e6 is a good range.
1818
export const DEFAULT_METADATA_MAX_ROWS_TO_READ = 3e6;
19+
const DEFAULT_MAX_KEYS = 1000;
1920

2021
export class MetadataCache {
2122
private cache = new Map<string, any>();
@@ -219,7 +220,7 @@ export class Metadata {
219220
databaseName,
220221
tableName,
221222
column,
222-
maxKeys = 1000,
223+
maxKeys = DEFAULT_MAX_KEYS,
223224
connectionId,
224225
metricName,
225226
}: {
@@ -304,6 +305,72 @@ export class Metadata {
304305
});
305306
}
306307

308+
async getJSONKeys({
309+
column,
310+
maxKeys = DEFAULT_MAX_KEYS,
311+
databaseName,
312+
tableName,
313+
connectionId,
314+
metricName,
315+
}: {
316+
column: string;
317+
maxKeys?: number;
318+
} & TableConnection) {
319+
const cacheKey = metricName
320+
? `${databaseName}.${tableName}.${column}.${metricName}.keys`
321+
: `${databaseName}.${tableName}.${column}.keys`;
322+
323+
return this.cache.getOrFetch<{ key: string; chType: string }[]>(
324+
cacheKey,
325+
async () => {
326+
const where = metricName
327+
? chSql`WHERE MetricName=${{ String: metricName }}`
328+
: '';
329+
const sql = chSql`WITH all_paths AS
330+
(
331+
SELECT DISTINCT JSONDynamicPathsWithTypes(${{ Identifier: column }}) as paths
332+
FROM ${tableExpr({ database: databaseName, table: tableName })} ${where}
333+
LIMIT ${{ Int32: maxKeys }}
334+
SETTINGS timeout_overflow_mode = 'break', max_execution_time = 2
335+
)
336+
SELECT groupUniqArrayMap(paths) as pathMap
337+
FROM all_paths;`;
338+
339+
const keys = await this.clickhouseClient
340+
.query<'JSON'>({
341+
query: sql.sql,
342+
query_params: sql.params,
343+
connectionId,
344+
clickhouse_settings: {
345+
max_rows_to_read: String(DEFAULT_METADATA_MAX_ROWS_TO_READ),
346+
read_overflow_mode: 'break',
347+
...this.clickhouseSettings,
348+
},
349+
})
350+
.then(res => res.json<{ pathMap: Record<string, string[]> }>())
351+
.then(d => {
352+
const keys: { key: string; chType: string }[] = [];
353+
for (const [key, typeArr] of Object.entries(d.data[0].pathMap)) {
354+
if (key || !typeArr || !Array.isArray(typeArr)) {
355+
throw new Error(
356+
`Error fetching keys for filters (key: ${key}, typeArr: ${typeArr})`,
357+
);
358+
}
359+
keys.push({
360+
key: key
361+
.split('.')
362+
.map(v => `\`${v}\``)
363+
.join('.'),
364+
chType: typeArr[0],
365+
});
366+
}
367+
return keys;
368+
});
369+
return keys;
370+
},
371+
);
372+
}
373+
307374
async getMapValues({
308375
databaseName,
309376
tableName,
@@ -391,10 +458,30 @@ export class Metadata {
391458
});
392459
}
393460

394-
const mapColumns = filterColumnMetaByType(columns, [JSDataType.Map]) ?? [];
461+
const mapColumns =
462+
filterColumnMetaByType(columns, [JSDataType.Map, JSDataType.JSON]) ?? [];
395463

396464
await Promise.all(
397465
mapColumns.map(async column => {
466+
if (convertCHDataTypeToJSType(column.type) === JSDataType.JSON) {
467+
const paths = await this.getJSONKeys({
468+
databaseName,
469+
tableName,
470+
column: column.name,
471+
connectionId,
472+
metricName,
473+
});
474+
475+
for (const path of paths) {
476+
fields.push({
477+
path: [column.name, path.key],
478+
type: path.chType,
479+
jsType: convertCHDataTypeToJSType(path.chType),
480+
});
481+
}
482+
return;
483+
}
484+
398485
const keys = await this.getMapKeys({
399486
databaseName,
400487
tableName,

0 commit comments

Comments
 (0)