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/smooth-schools-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hyperdx/common-utils": patch
"@hyperdx/app": patch
---

fix: Fix ascending order in windowed searches
7 changes: 6 additions & 1 deletion packages/app/src/components/DBRowTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,11 @@ function DBSqlRowTableComponent({
groupedPatterns.isLoading
: isFetching;

const loadingDate =
data?.window?.direction === 'ASC'
? data?.window?.endTime
: data?.window?.startTime;

return (
<>
{denoiseResults && (
Expand Down Expand Up @@ -1299,7 +1304,7 @@ function DBSqlRowTableComponent({
error={error ?? undefined}
columnTypeMap={columnMap}
dateRange={config.dateRange}
loadingDate={data?.window?.startTime}
loadingDate={loadingDate}
config={config}
onChildModalOpen={onChildModalOpen}
source={source}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const createMockChartConfig = (
overrides: Partial<ChartConfigWithDateRange> = {},
): ChartConfigWithDateRange =>
({
timestampValueExpression: '',
timestampValueExpression: 'Timestamp',
connection: 'foo',
from: {
databaseName: 'telemetry',
Expand All @@ -55,6 +55,7 @@ const createMockChartConfig = (
limit: 100,
offset: 0,
},
orderBy: 'Timestamp DESC',
...overrides,
}) as ChartConfigWithDateRange;

Expand Down Expand Up @@ -153,6 +154,87 @@ describe('useOffsetPaginatedQuery', () => {
expect(result.current.data?.window.endTime).toEqual(
new Date('2024-01-02T00:00:00Z'), // endDate
);
expect(result.current.data?.window.direction).toEqual('DESC');
});

it('should generate correct time windows for 24-hour range with ascending sort order', async () => {
const config = createMockChartConfig({
dateRange: [
new Date('2024-01-01T00:00:00Z'),
new Date('2024-01-02T00:00:00Z'),
] as [Date, Date],
orderBy: 'Timestamp ASC',
});

// Mock the reader to return data for first window
mockReader.read
.mockResolvedValueOnce({
done: false,
value: [
{ json: () => ['timestamp', 'message'] },
{ json: () => ['DateTime', 'String'] },
{ json: () => ['2024-01-01T01:00:00Z', 'test log 1'] },
{ json: () => ['2024-01-01T02:00:00Z', 'test log 2'] },
],
})
.mockResolvedValueOnce({ done: true });

const { result } = renderHook(() => useOffsetPaginatedQuery(config), {
wrapper,
});

await waitFor(() => expect(result.current.isLoading).toBe(false));

// Should have data from the first 6-hour window (working forwards from start date)
expect(result.current.data).toBeDefined();
expect(result.current.data?.window.windowIndex).toBe(0);
expect(result.current.data?.window.startTime).toEqual(
new Date('2024-01-01T00:00:00Z'), // startDate
);
expect(result.current.data?.window.endTime).toEqual(
new Date('2024-01-01T06:00:00Z'), // endDate + 6h
);
expect(result.current.data?.window.direction).toEqual('ASC');
});

it('should not use time windows if first ordering is not on timestamp', async () => {
const config = createMockChartConfig({
dateRange: [
new Date('2024-01-01T00:00:00Z'),
new Date('2024-01-02T00:00:00Z'),
] as [Date, Date],
orderBy: 'ServiceName',
});

// Mock the reader to return data for first window
mockReader.read
.mockResolvedValueOnce({
done: false,
value: [
{ json: () => ['timestamp', 'message'] },
{ json: () => ['DateTime', 'String'] },
{ json: () => ['2024-01-01T01:00:00Z', 'test log 1'] },
{ json: () => ['2024-01-01T02:00:00Z', 'test log 2'] },
],
})
.mockResolvedValueOnce({ done: true });

const { result } = renderHook(() => useOffsetPaginatedQuery(config), {
wrapper,
});

await waitFor(() => expect(result.current.isLoading).toBe(false));

// Should have data from the entire range, without windowing
expect(result.current.data).toBeDefined();
expect(result.current.data?.window.windowIndex).toBe(0);
expect(result.current.data?.window.startTime).toEqual(
new Date('2024-01-01T00:00:00Z'), // startDate
);
expect(result.current.data?.window.endTime).toEqual(
new Date('2024-01-02T00:00:00Z'), // endDate + 6h
);
expect(result.current.data?.window.direction).toEqual('DESC');
});

it('should handle very large time ranges with progressive bucketing', async () => {
Expand Down
59 changes: 54 additions & 5 deletions packages/app/src/hooks/useOffsetPaginatedQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import {
} from '@hyperdx/common-utils/dist/clickhouse';
import { renderChartConfig } from '@hyperdx/common-utils/dist/renderChartConfig';
import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types';
import {
isFirstOrderByAscending,
isTimestampExpressionInFirstOrderBy,
} from '@hyperdx/common-utils/dist/utils';
import {
QueryClient,
QueryFunction,
Expand Down Expand Up @@ -45,6 +49,7 @@ type TimeWindow = {
startTime: Date;
endTime: Date;
windowIndex: number;
direction: 'ASC' | 'DESC';
};

type TPageParam = {
Expand All @@ -64,8 +69,11 @@ type TData = {
pageParams: TPageParam[];
};

// Generate time windows from date range using progressive bucketing
function generateTimeWindows(startDate: Date, endDate: Date): TimeWindow[] {
// Generate time windows from date range using progressive bucketing, starting at the end of the date range
function generateTimeWindowsDescending(
startDate: Date,
endDate: Date,
): TimeWindow[] {
const windows: TimeWindow[] = [];
let currentEnd = new Date(endDate);
let windowIndex = 0;
Expand All @@ -82,6 +90,7 @@ function generateTimeWindows(startDate: Date, endDate: Date): TimeWindow[] {
endTime: new Date(currentEnd),
startTime: windowStart,
windowIndex,
direction: 'DESC',
});

currentEnd = windowStart;
Expand All @@ -91,13 +100,43 @@ function generateTimeWindows(startDate: Date, endDate: Date): TimeWindow[] {
return windows;
}

// Generate time windows from date range using progressive bucketing, starting at the beginning of the date range
function generateTimeWindowsAscending(startDate: Date, endDate: Date) {
const windows: TimeWindow[] = [];
let currentStart = new Date(startDate);
let windowIndex = 0;

while (currentStart < endDate) {
const windowSize =
TIME_WINDOWS_MS[windowIndex] ||
TIME_WINDOWS_MS[TIME_WINDOWS_MS.length - 1]; // use largest window size
const windowEnd = new Date(
Math.min(currentStart.getTime() + windowSize, endDate.getTime()),
);

windows.push({
startTime: new Date(currentStart),
endTime: windowEnd,
windowIndex,
direction: 'ASC',
});

currentStart = windowEnd;
windowIndex++;
}

return windows;
}

// Get time window from page param
function getTimeWindowFromPageParam(
config: ChartConfigWithDateRange,
pageParam: TPageParam,
): TimeWindow {
const [startDate, endDate] = config.dateRange;
const windows = generateTimeWindows(startDate, endDate);
const windows = isFirstOrderByAscending(config.orderBy)
? generateTimeWindowsAscending(startDate, endDate)
: generateTimeWindowsDescending(startDate, endDate);
const window = windows[pageParam.windowIndex];
if (window == null) {
throw new Error('Invalid time window for page param');
Expand All @@ -116,7 +155,9 @@ function getNextPageParam(
}

const [startDate, endDate] = config.dateRange;
const windows = generateTimeWindows(startDate, endDate);
const windows = isFirstOrderByAscending(config.orderBy)
? generateTimeWindowsAscending(startDate, endDate)
: generateTimeWindowsDescending(startDate, endDate);
const currentWindow = lastPage.window;

// Calculate total results from all pages in current window
Expand Down Expand Up @@ -169,7 +210,15 @@ const queryFn: QueryFunction<TQueryFnData, TQueryKey, TPageParam> = async ({
const config = queryKey[1];

// Get the time window for this page
const timeWindow = getTimeWindowFromPageParam(config, pageParam);
const shouldUseWindowing = isTimestampExpressionInFirstOrderBy(config);
const timeWindow = shouldUseWindowing
? getTimeWindowFromPageParam(config, pageParam)
: {
startTime: config.dateRange[0],
endTime: config.dateRange[1],
windowIndex: 0,
direction: 'DESC' as const,
};

// Create config with windowed date range
const windowedConfig = {
Expand Down
Loading