Skip to content

Commit 6229d52

Browse files
committed
feat: Add disableQueryChunking option
1 parent dd2d0fe commit 6229d52

File tree

4 files changed

+162
-8
lines changed

4 files changed

+162
-8
lines changed

packages/app/src/hooks/__tests__/useChartConfig.test.tsx

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ describe('useChartConfig', () => {
7474
expect(
7575
getGranularityAlignedTimeWindows({
7676
granularity: '1 hour',
77+
timestampValueExpression: 'TimestampTime',
7778
}),
7879
).toEqual([undefined]);
7980
});
@@ -82,6 +83,16 @@ describe('useChartConfig', () => {
8283
expect(
8384
getGranularityAlignedTimeWindows({
8485
dateRange: [new Date('2023-01-01'), new Date('2023-01-02')],
86+
timestampValueExpression: 'TimestampTime',
87+
}),
88+
).toEqual([undefined]);
89+
});
90+
91+
it('returns [undefined] if no timestampValueExpression is provided', () => {
92+
expect(
93+
getGranularityAlignedTimeWindows({
94+
dateRange: [new Date('2023-01-01'), new Date('2023-01-02')],
95+
granularity: '1 hour',
8596
}),
8697
).toEqual([undefined]);
8798
});
@@ -95,6 +106,7 @@ describe('useChartConfig', () => {
95106
new Date('2023-01-10 00:10:00'),
96107
],
97108
granularity: '1 minute',
109+
timestampValueExpression: 'TimestampTime',
98110
},
99111
[
100112
30, // 30s
@@ -143,6 +155,7 @@ describe('useChartConfig', () => {
143155
new Date('2023-01-10 00:10:00'),
144156
],
145157
granularity: '1 minute',
158+
timestampValueExpression: 'TimestampTime',
146159
},
147160
[
148161
15, // 15s
@@ -175,6 +188,7 @@ describe('useChartConfig', () => {
175188
new Date('2023-01-10 00:00:30'),
176189
],
177190
granularity: '1 minute',
191+
timestampValueExpression: 'TimestampTime',
178192
dateRangeEndInclusive: true,
179193
},
180194
[
@@ -230,6 +244,7 @@ describe('useChartConfig', () => {
230244
new Date('2023-01-10 00:02:00'),
231245
],
232246
granularity: '1 minute',
247+
timestampValueExpression: 'TimestampTime',
233248
},
234249
[
235250
60, // 1m
@@ -373,6 +388,104 @@ describe('useChartConfig', () => {
373388
expect(result.current.isPending).toBe(false);
374389
});
375390

391+
it('fetches data without chunking when no timestampValueExpression is provided', async () => {
392+
const config = createMockChartConfig({
393+
dateRange: [
394+
new Date('2025-10-01 00:00:00Z'),
395+
new Date('2025-10-02 00:00:00Z'),
396+
],
397+
granularity: '1 hour',
398+
timestampValueExpression: undefined,
399+
});
400+
401+
const mockResponse = createMockQueryResponse([
402+
{
403+
'count()': '71',
404+
SeverityText: 'info',
405+
__hdx_time_bucket: '2025-10-01T00:00:00Z',
406+
},
407+
{
408+
'count()': '73',
409+
SeverityText: 'info',
410+
__hdx_time_bucket: '2025-10-02T00:00:00Z',
411+
},
412+
]);
413+
414+
mockClickhouseClient.queryChartConfig.mockResolvedValue(mockResponse);
415+
416+
const { result } = renderHook(() => useQueriedChartConfig(config), {
417+
wrapper,
418+
});
419+
420+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
421+
await waitFor(() => expect(result.current.isFetching).toBe(false));
422+
423+
// Should only be called once since chunking is disabled without timestampValueExpression
424+
expect(mockClickhouseClient.queryChartConfig).toHaveBeenCalledTimes(1);
425+
expect(mockClickhouseClient.queryChartConfig).toHaveBeenCalledWith({
426+
config,
427+
metadata: expect.any(Object),
428+
opts: {
429+
abort_signal: expect.any(AbortSignal),
430+
},
431+
});
432+
expect(result.current.data).toEqual({
433+
data: mockResponse.data,
434+
meta: mockResponse.meta,
435+
rows: mockResponse.rows,
436+
});
437+
});
438+
439+
it('fetches data without chunking when disableQueryChunking is true', async () => {
440+
const config = createMockChartConfig({
441+
dateRange: [
442+
new Date('2025-10-01 00:00:00Z'),
443+
new Date('2025-10-02 00:00:00Z'),
444+
],
445+
granularity: '1 hour',
446+
});
447+
448+
const mockResponse = createMockQueryResponse([
449+
{
450+
'count()': '71',
451+
SeverityText: 'info',
452+
__hdx_time_bucket: '2025-10-01T00:00:00Z',
453+
},
454+
{
455+
'count()': '73',
456+
SeverityText: 'info',
457+
__hdx_time_bucket: '2025-10-02T00:00:00Z',
458+
},
459+
]);
460+
461+
mockClickhouseClient.queryChartConfig.mockResolvedValue(mockResponse);
462+
463+
const { result } = renderHook(
464+
() => useQueriedChartConfig(config, { disableQueryChunking: true }),
465+
{
466+
wrapper,
467+
},
468+
);
469+
470+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
471+
await waitFor(() => expect(result.current.isFetching).toBe(false));
472+
473+
// Should only be called once since chunking is explicitly disabled
474+
expect(mockClickhouseClient.queryChartConfig).toHaveBeenCalledTimes(1);
475+
expect(mockClickhouseClient.queryChartConfig).toHaveBeenCalledWith({
476+
config,
477+
metadata: expect.any(Object),
478+
opts: {
479+
abort_signal: expect.any(AbortSignal),
480+
},
481+
});
482+
expect(result.current.data).toEqual({
483+
data: mockResponse.data,
484+
meta: mockResponse.meta,
485+
rows: mockResponse.rows,
486+
});
487+
});
488+
376489
it('fetches data with chunking when granularity and date range are provided', async () => {
377490
const config = createMockChartConfig({
378491
dateRange: [
@@ -521,6 +634,8 @@ describe('useChartConfig', () => {
521634
rows: 4,
522635
});
523636
expect(result.current.isFetching).toBe(true);
637+
expect(result.current.isLoading).toBe(false); // isLoading is false because we have partial data
638+
expect(result.current.isSuccess).toBe(true); // isSuccess is true because we have partial data
524639

525640
// Resolve the final promise to simulate data arriving
526641
const mockResponse3Data = createMockQueryResponse([

packages/app/src/hooks/useChartConfig.tsx

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/browser';
88
import {
99
DEFAULT_AUTO_GRANULARITY_MAX_BUCKETS,
10+
isUsingGranularity,
1011
renderChartConfig,
1112
} from '@hyperdx/common-utils/dist/renderChartConfig';
1213
import { format } from '@hyperdx/common-utils/dist/sqlFormatter';
@@ -29,6 +30,13 @@ import { generateTimeWindowsDescending } from '@/utils/searchWindows';
2930

3031
interface AdditionalUseQueriedChartConfigOptions {
3132
onError?: (error: Error | ClickHouseQueryError) => void;
33+
/**
34+
* By default, queries with large date ranges are split into multiple smaller queries to
35+
* avoid overloading the ClickHouse server and running into timeouts. In some cases, such
36+
* as when data is being sampled across the entire range, this chunking is not desirable
37+
* and can be disabled.
38+
*/
39+
disableQueryChunking?: boolean;
3240
}
3341

3442
type TimeWindow = {
@@ -41,13 +49,18 @@ type TQueryFnData = Pick<ResponseJSON<any>, 'data' | 'meta' | 'rows'> & {};
4149
export const getGranularityAlignedTimeWindows = (
4250
config: Pick<
4351
ChartConfigWithOptDateRange,
44-
'dateRange' | 'granularity' | 'dateRangeEndInclusive'
52+
| 'dateRange'
53+
| 'granularity'
54+
| 'dateRangeEndInclusive'
55+
| 'timestampValueExpression'
4556
>,
4657
windowDurationsSeconds?: number[],
4758
): TimeWindow[] | [undefined] => {
48-
// Granularity is required for pagination, otherwise we could break other group-bys.
49-
// Date range is required for pagination, otherwise we'd have infinite pages, or some unbounded page(s).
50-
if (!config.dateRange || !config.granularity) return [undefined];
59+
// Granularity is required for chunking, otherwise we could break other group-bys.
60+
if (!isUsingGranularity(config)) return [undefined];
61+
62+
// Date range is required for chunking, otherwise we'd have infinite chunks, or some unbounded chunk(s).
63+
if (!config.dateRange) return [undefined];
5164

5265
const [startDate, endDate] = config.dateRange;
5366
const windowsUnaligned = generateTimeWindowsDescending(
@@ -95,8 +108,11 @@ async function* fetchDataInChunks(
95108
config: ChartConfigWithOptDateRange,
96109
clickhouseClient: ClickhouseClient,
97110
signal: AbortSignal,
111+
disableQueryChunking: boolean = false,
98112
) {
99-
const windows = getGranularityAlignedTimeWindows(config);
113+
const windows = disableQueryChunking
114+
? ([undefined] as const)
115+
: getGranularityAlignedTimeWindows(config);
100116

101117
let query = null;
102118
if (IS_MTVIEWS_ENABLED) {
@@ -128,6 +144,20 @@ async function* fetchDataInChunks(
128144
}
129145
}
130146

147+
/**
148+
* A hook providing data queried based on the provided chart config.
149+
*
150+
* If the config includes a date range and granularity, the query will be chunked
151+
* into multiple smaller queries aligned to the granularity to improve performance.
152+
*
153+
* Note that the limit is applied to each chunk, so the total number of rows returned
154+
* may be up to `limit * number_of_chunks`.
155+
*
156+
* Also note that the returned data will be ordered within each chunk, and chunks will
157+
* be ordered oldest-first, by the timestampValueExpression.
158+
*
159+
* To opt-out of chunking, pass `disableQueryChunking: true` in the options.
160+
*/
131161
export function useQueriedChartConfig(
132162
config: ChartConfigWithOptDateRange,
133163
options?: Partial<UseQueryOptions<ResponseJSON<any>>> &
@@ -139,7 +169,12 @@ export function useQueriedChartConfig(
139169
queryKey: [config],
140170
queryFn: streamedQuery({
141171
streamFn: context =>
142-
fetchDataInChunks(config, clickhouseClient, context.signal),
172+
fetchDataInChunks(
173+
config,
174+
clickhouseClient,
175+
context.signal,
176+
options?.disableQueryChunking,
177+
),
143178
/**
144179
* This mode ensures that data remains in the cache until the next full streamed result is available.
145180
* By default, the cache would be cleared before new data starts arriving, which results in the query briefly

packages/app/src/hooks/usePatterns.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,11 @@ function usePatterns({
143143

144144
const { data: sampleRows } = useQueriedChartConfig(
145145
configWithPrimaryAndPartitionKey ?? config, // `config` satisfying type, never used due to `enabled` check
146-
{ enabled: configWithPrimaryAndPartitionKey != null && enabled },
146+
{
147+
enabled: configWithPrimaryAndPartitionKey != null && enabled,
148+
// Disable chunking so that we get a uniform sample across the entire date range
149+
disableQueryChunking: true,
150+
},
147151
);
148152

149153
const { data: pyodide, isLoading: isLoadingPyodide } = usePyodide({

packages/common-utils/src/renderChartConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function isUsingGroupBy(
5858
return chartConfig.groupBy != null && chartConfig.groupBy.length > 0;
5959
}
6060

61-
function isUsingGranularity(
61+
export function isUsingGranularity(
6262
chartConfig: ChartConfigWithOptDateRange,
6363
): chartConfig is Omit<
6464
Omit<Omit<ChartConfigWithDateRange, 'granularity'>, 'dateRange'>,

0 commit comments

Comments
 (0)