Skip to content

Commit 9c4c5f4

Browse files
authored
feat: support toUnixTimestamp style timestamps in ORDER BY (#1116)
1 parent d6f8058 commit 9c4c5f4

File tree

3 files changed

+77
-120
lines changed

3 files changed

+77
-120
lines changed

.changeset/lemon-pans-play.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
feat: support toUnixTimestamp style timestamps in ORDER BY

packages/app/src/DBSearchPage.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -510,20 +510,27 @@ function optimizeDefaultOrderBy(
510510
const fallbackOrderBy = fallbackOrderByItems.join(' ');
511511
if (!sortingKey) return fallbackOrderBy;
512512

513+
const orderByArr = [];
513514
const sortKeys = sortingKey.split(',').map(key => key.trim());
514-
const timestampExprIdx = sortKeys.findIndex(v => v === timestampExpr);
515-
if (timestampExprIdx <= 0) return fallbackOrderBy;
516-
517-
const orderByArr = [fallbackOrderByItems[0]];
518-
for (let i = 0; i < timestampExprIdx; i++) {
515+
for (let i = 0; i < sortKeys.length; i++) {
519516
const sortKey = sortKeys[i];
520517
if (sortKey.includes('toStartOf') && sortKey.includes(timestampExpr)) {
521518
orderByArr.push(sortKey);
519+
} else if (
520+
sortKey === timestampExpr ||
521+
(sortKey.startsWith('toUnixTimestamp') && sortKey.includes(timestampExpr))
522+
) {
523+
if (i === 0) {
524+
// fallback if the first sort key is the timestamp sort key
525+
return fallbackOrderBy;
526+
} else {
527+
orderByArr.push(sortKey);
528+
break;
529+
}
522530
}
523531
}
524532

525-
const newOrderBy = `(${orderByArr.reverse().join(', ')}) ${defaultModifier}`;
526-
return newOrderBy;
533+
return `(${orderByArr.join(', ')}) ${defaultModifier}`;
527534
}
528535

529536
export function useDefaultOrderBy(sourceID: string | undefined | null) {

packages/app/src/__tests__/DBSearchPage.test.tsx

Lines changed: 58 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -16,126 +16,71 @@ describe('useDefaultOrderBy', () => {
1616
});
1717

1818
describe('optimizeOrderBy function', () => {
19-
it('should return fallback order by when tableMetadata is not available', () => {
19+
it('should handle these cases', () => {
2020
const mockSource = {
2121
timestampValueExpression: 'Timestamp',
2222
};
2323

24-
jest.spyOn(sourceModule, 'useSource').mockReturnValue({
25-
data: mockSource,
26-
isLoading: false,
27-
error: null,
28-
} as any);
29-
30-
jest.spyOn(metadataModule, 'useTableMetadata').mockReturnValue({
31-
data: undefined,
32-
isLoading: false,
33-
error: null,
34-
} as any);
35-
36-
const { result } = renderHook(() => useDefaultOrderBy('source-id'));
37-
38-
expect(result.current).toBe('Timestamp DESC');
39-
});
40-
41-
it('should handle empty Timestamp expression ungracefully', () => {
42-
const mockSource = {
43-
timestampValueExpression: '',
44-
};
45-
46-
jest.spyOn(sourceModule, 'useSource').mockReturnValue({
47-
data: mockSource,
48-
isLoading: false,
49-
error: null,
50-
} as any);
51-
52-
jest.spyOn(metadataModule, 'useTableMetadata').mockReturnValue({
53-
data: undefined,
54-
isLoading: false,
55-
error: null,
56-
} as any);
57-
58-
const { result } = renderHook(() => useDefaultOrderBy('source-id'));
59-
60-
expect(result.current).toBe(' DESC');
61-
});
62-
63-
it('should return optimized order by when Timestamp is not first in sorting key', () => {
64-
const mockSource = {
65-
timestampValueExpression: 'Timestamp',
66-
};
67-
68-
const mockTableMetadata = {
69-
sorting_key: 'toStartOfHour(Timestamp), other_column, Timestamp',
70-
};
71-
72-
jest.spyOn(sourceModule, 'useSource').mockReturnValue({
73-
data: mockSource,
74-
isLoading: false,
75-
error: null,
76-
} as any);
77-
78-
jest.spyOn(metadataModule, 'useTableMetadata').mockReturnValue({
79-
data: mockTableMetadata,
80-
isLoading: false,
81-
error: null,
82-
} as any);
83-
84-
const { result } = renderHook(() => useDefaultOrderBy('source-id'));
85-
86-
expect(result.current).toBe('(toStartOfHour(Timestamp), Timestamp) DESC');
87-
});
88-
89-
it('should return fallback when Timestamp is first in sorting key', () => {
90-
const mockSource = {
91-
timestampValueExpression: 'Timestamp',
92-
};
93-
94-
const mockTableMetadata = {
95-
sorting_key: 'Timestamp, other_column',
96-
};
97-
98-
jest.spyOn(sourceModule, 'useSource').mockReturnValue({
99-
data: mockSource,
100-
isLoading: false,
101-
error: null,
102-
} as any);
103-
104-
jest.spyOn(metadataModule, 'useTableMetadata').mockReturnValue({
105-
data: mockTableMetadata,
106-
isLoading: false,
107-
error: null,
108-
} as any);
109-
110-
const { result } = renderHook(() => useDefaultOrderBy('source-id'));
111-
112-
expect(result.current).toBe('Timestamp DESC');
113-
});
114-
115-
it('should ignore non-toStartOf columns before Timestamp', () => {
116-
const mockSource = {
117-
timestampValueExpression: 'Timestamp',
118-
};
119-
120-
const mockTableMetadata = {
121-
sorting_key: 'user_id, toStartOfHour(Timestamp), status, Timestamp',
122-
};
123-
124-
jest.spyOn(sourceModule, 'useSource').mockReturnValue({
125-
data: mockSource,
126-
isLoading: false,
127-
error: null,
128-
} as any);
24+
const testCases = [
25+
{
26+
input: undefined,
27+
expected: 'Timestamp DESC',
28+
},
29+
{
30+
input: '',
31+
expected: 'Timestamp DESC',
32+
},
33+
{
34+
input: 'toStartOfHour(Timestamp), other_column, Timestamp',
35+
expected: '(toStartOfHour(Timestamp), Timestamp) DESC',
36+
},
37+
{
38+
input: 'Timestamp, other_column',
39+
expected: 'Timestamp DESC',
40+
},
41+
{
42+
input: 'user_id, toStartOfHour(Timestamp), status, Timestamp',
43+
expected: '(toStartOfHour(Timestamp), Timestamp) DESC',
44+
},
45+
{
46+
input:
47+
'toStartOfMinute(Timestamp), user_id, status, toUnixTimestamp(Timestamp)',
48+
expected:
49+
'(toStartOfMinute(Timestamp), toUnixTimestamp(Timestamp)) DESC',
50+
},
51+
{
52+
// test variation of toUnixTimestamp
53+
input:
54+
'toStartOfMinute(Timestamp), user_id, status, toUnixTimestamp64Nano(Timestamp)',
55+
expected:
56+
'(toStartOfMinute(Timestamp), toUnixTimestamp64Nano(Timestamp)) DESC',
57+
},
58+
{
59+
input:
60+
'toUnixTimestamp(toStartOfMinute(Timestamp)), user_id, status, Timestamp',
61+
expected:
62+
'(toUnixTimestamp(toStartOfMinute(Timestamp)), Timestamp) DESC',
63+
},
64+
];
65+
for (const testCase of testCases) {
66+
const mockTableMetadata = { sorting_key: testCase.input };
67+
68+
jest.spyOn(sourceModule, 'useSource').mockReturnValue({
69+
data: mockSource,
70+
isLoading: false,
71+
error: null,
72+
} as any);
12973

130-
jest.spyOn(metadataModule, 'useTableMetadata').mockReturnValue({
131-
data: mockTableMetadata,
132-
isLoading: false,
133-
error: null,
134-
} as any);
74+
jest.spyOn(metadataModule, 'useTableMetadata').mockReturnValue({
75+
data: mockTableMetadata,
76+
isLoading: false,
77+
error: null,
78+
} as any);
13579

136-
const { result } = renderHook(() => useDefaultOrderBy('source-id'));
80+
const { result } = renderHook(() => useDefaultOrderBy('source-id'));
13781

138-
expect(result.current).toBe('(toStartOfHour(Timestamp), Timestamp) DESC');
82+
expect(result.current).toBe(testCase.expected);
83+
}
13984
});
14085

14186
it('should handle null source ungracefully', () => {

0 commit comments

Comments
 (0)