Skip to content

Commit 779f63c

Browse files
committed
feat: Optimize and fix filtering on toStartOfX primary key expressions
1 parent 43e32aa commit 779f63c

File tree

5 files changed

+538
-17
lines changed

5 files changed

+538
-17
lines changed

.changeset/fluffy-mails-sparkle.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: Optimize and fix filtering on toStartOfX primary key expressions

packages/common-utils/src/__tests__/renderChartConfig.test.ts

Lines changed: 207 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { chSql, parameterizedQueryToSql } from '@/clickhouse';
1+
import { chSql, ColumnMeta, parameterizedQueryToSql } from '@/clickhouse';
22
import { Metadata } from '@/metadata';
33
import {
44
ChartConfigWithOptDateRange,
55
DisplayType,
66
MetricsDataType,
77
} from '@/types';
88

9-
import { renderChartConfig } from '../renderChartConfig';
9+
import { renderChartConfig, timeFilterExpr } from '../renderChartConfig';
1010

1111
describe('renderChartConfig', () => {
12-
let mockMetadata: Metadata;
12+
let mockMetadata: jest.Mocked<Metadata>;
1313

1414
beforeEach(() => {
1515
mockMetadata = {
@@ -19,7 +19,10 @@ describe('renderChartConfig', () => {
1919
]),
2020
getMaterializedColumnsLookupTable: jest.fn().mockResolvedValue(null),
2121
getColumn: jest.fn().mockResolvedValue({ type: 'DateTime' }),
22-
} as unknown as Metadata;
22+
getTableMetadata: jest
23+
.fn()
24+
.mockResolvedValue({ primary_key: 'timestamp' }),
25+
} as unknown as jest.Mocked<Metadata>;
2326
});
2427

2528
const gaugeConfiguration: ChartConfigWithOptDateRange = {
@@ -630,4 +633,204 @@ describe('renderChartConfig', () => {
630633
expect(actual).toMatchSnapshot();
631634
});
632635
});
636+
637+
describe('timeFilterExpr', () => {
638+
type TimeFilterExprTestCase = {
639+
timestampValueExpression: string;
640+
dateRangeStartInclusive?: boolean;
641+
dateRangeEndInclusive?: boolean;
642+
dateRange: [Date, Date];
643+
includedDataInterval?: string;
644+
expected: string;
645+
description: string;
646+
tableName?: string;
647+
databaseName?: string;
648+
primaryKey?: string;
649+
};
650+
651+
const testCases: TimeFilterExprTestCase[] = [
652+
{
653+
description: 'with basic timestampValueExpression',
654+
timestampValueExpression: 'timestamp',
655+
dateRange: [
656+
new Date('2025-02-12 00:12:34Z'),
657+
new Date('2025-02-14 00:12:34Z'),
658+
],
659+
expected: `(timestamp >= fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}) AND timestamp <= fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}))`,
660+
},
661+
{
662+
description: 'with dateRangeEndInclusive=false',
663+
timestampValueExpression: 'timestamp',
664+
dateRange: [
665+
new Date('2025-02-12 00:12:34Z'),
666+
new Date('2025-02-14 00:12:34Z'),
667+
],
668+
dateRangeEndInclusive: false,
669+
expected: `(timestamp >= fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}) AND timestamp < fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}))`,
670+
},
671+
{
672+
description: 'with dateRangeStartInclusive=false',
673+
timestampValueExpression: 'timestamp',
674+
dateRange: [
675+
new Date('2025-02-12 00:12:34Z'),
676+
new Date('2025-02-14 00:12:34Z'),
677+
],
678+
dateRangeStartInclusive: false,
679+
expected: `(timestamp > fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}) AND timestamp <= fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}))`,
680+
},
681+
{
682+
description: 'with includedDataInterval',
683+
timestampValueExpression: 'timestamp',
684+
dateRange: [
685+
new Date('2025-02-12 00:12:34Z'),
686+
new Date('2025-02-14 00:12:34Z'),
687+
],
688+
includedDataInterval: '1 WEEK',
689+
expected: `(timestamp >= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}), INTERVAL 1 WEEK) - INTERVAL 1 WEEK AND timestamp <= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}), INTERVAL 1 WEEK) + INTERVAL 1 WEEK)`,
690+
},
691+
{
692+
description: 'with date type timestampValueExpression',
693+
timestampValueExpression: 'date',
694+
dateRange: [
695+
new Date('2025-02-12 00:12:34Z'),
696+
new Date('2025-02-14 00:12:34Z'),
697+
],
698+
expected: `(date >= toDate(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()})) AND date <= toDate(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()})))`,
699+
},
700+
{
701+
description: 'with multiple timestampValueExpression parts',
702+
timestampValueExpression: 'timestamp, date',
703+
dateRange: [
704+
new Date('2025-02-12 00:12:34Z'),
705+
new Date('2025-02-14 00:12:34Z'),
706+
],
707+
expected: `(timestamp >= fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}) AND timestamp <= fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}))AND(date >= toDate(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()})) AND date <= toDate(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()})))`,
708+
},
709+
{
710+
description: 'with toStartOfDay() in timestampExpr',
711+
timestampValueExpression: 'toStartOfDay(timestamp)',
712+
dateRange: [
713+
new Date('2025-02-12 00:12:34Z'),
714+
new Date('2025-02-14 00:12:34Z'),
715+
],
716+
expected: `(toStartOfDay(timestamp) >= toStartOfDay(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()})) AND toStartOfDay(timestamp) <= toStartOfDay(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()})))`,
717+
},
718+
{
719+
description: 'with toStartOfDay () in timestampExpr',
720+
timestampValueExpression: 'toStartOfDay (timestamp)',
721+
dateRange: [
722+
new Date('2025-02-12 00:12:34Z'),
723+
new Date('2025-02-14 00:12:34Z'),
724+
],
725+
expected: `(toStartOfDay (timestamp) >= toStartOfDay(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()})) AND toStartOfDay (timestamp) <= toStartOfDay(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()})))`,
726+
},
727+
{
728+
description: 'with toStartOfInterval() in timestampExpr',
729+
timestampValueExpression:
730+
'toStartOfInterval(timestamp, INTERVAL 12 MINUTE)',
731+
dateRange: [
732+
new Date('2025-02-12 00:12:34Z'),
733+
new Date('2025-02-14 00:12:34Z'),
734+
],
735+
expected: `(toStartOfInterval(timestamp, INTERVAL 12 MINUTE) >= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}), INTERVAL 12 MINUTE) AND toStartOfInterval(timestamp, INTERVAL 12 MINUTE) <= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}), INTERVAL 12 MINUTE))`,
736+
},
737+
{
738+
description:
739+
'with toStartOfInterval() with lowercase interval in timestampExpr',
740+
timestampValueExpression:
741+
'toStartOfInterval(timestamp, interval 1 minute)',
742+
dateRange: [
743+
new Date('2025-02-12 00:12:34Z'),
744+
new Date('2025-02-14 00:12:34Z'),
745+
],
746+
expected: `(toStartOfInterval(timestamp, interval 1 minute) >= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}), interval 1 minute) AND toStartOfInterval(timestamp, interval 1 minute) <= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}), interval 1 minute))`,
747+
},
748+
{
749+
description: 'with toStartOfInterval() with timezone and offset',
750+
timestampValueExpression: `toStartOfInterval(timestamp, INTERVAL 1 MINUTE, toDateTime('2023-01-01 14:35:30'), 'America/New_York')`,
751+
dateRange: [
752+
new Date('2025-02-12 00:12:34Z'),
753+
new Date('2025-02-14 00:12:34Z'),
754+
],
755+
expected: `(toStartOfInterval(timestamp, INTERVAL 1 MINUTE, toDateTime('2023-01-01 14:35:30'), 'America/New_York') >= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}), INTERVAL 1 MINUTE, toDateTime('2023-01-01 14:35:30'), 'America/New_York') AND toStartOfInterval(timestamp, INTERVAL 1 MINUTE, toDateTime('2023-01-01 14:35:30'), 'America/New_York') <= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}), INTERVAL 1 MINUTE, toDateTime('2023-01-01 14:35:30'), 'America/New_York'))`,
756+
},
757+
{
758+
description: 'with nonstandard spacing',
759+
timestampValueExpression: ` toStartOfInterval ( timestamp , INTERVAL 1 MINUTE , toDateTime ( '2023-01-01 14:35:30' ), 'America/New_York' ) `,
760+
dateRange: [
761+
new Date('2025-02-12 00:12:34Z'),
762+
new Date('2025-02-14 00:12:34Z'),
763+
],
764+
expected: `(toStartOfInterval ( timestamp , INTERVAL 1 MINUTE , toDateTime ( '2023-01-01 14:35:30' ), 'America/New_York' ) >= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}), INTERVAL 1 MINUTE, toDateTime ( '2023-01-01 14:35:30' ), 'America/New_York') AND toStartOfInterval ( timestamp , INTERVAL 1 MINUTE , toDateTime ( '2023-01-01 14:35:30' ), 'America/New_York' ) <= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}), INTERVAL 1 MINUTE, toDateTime ( '2023-01-01 14:35:30' ), 'America/New_York'))`,
765+
},
766+
{
767+
description: 'with optimizable timestampValueExpression',
768+
timestampValueExpression: `timestamp`,
769+
primaryKey:
770+
"toStartOfMinute(timestamp), ServiceName, ResourceAttributes['timestamp'], timestamp",
771+
dateRange: [
772+
new Date('2025-02-12 00:12:34Z'),
773+
new Date('2025-02-14 00:12:34Z'),
774+
],
775+
expected: `(timestamp >= fromUnixTimestamp64Milli(1739319154000) AND timestamp <= fromUnixTimestamp64Milli(1739491954000))AND(toStartOfMinute(timestamp) >= toStartOfMinute(fromUnixTimestamp64Milli(1739319154000)) AND toStartOfMinute(timestamp) <= toStartOfMinute(fromUnixTimestamp64Milli(1739491954000)))`,
776+
},
777+
{
778+
description: 'with synthetic timestamp value expression for CTE',
779+
timestampValueExpression: `__hdx_time_bucket`,
780+
dateRange: [
781+
new Date('2025-02-12 00:12:34Z'),
782+
new Date('2025-02-14 00:12:34Z'),
783+
],
784+
databaseName: '',
785+
tableName: 'Bucketed',
786+
primaryKey:
787+
"toStartOfMinute(timestamp), ServiceName, ResourceAttributes['timestamp'], timestamp",
788+
expected: `(__hdx_time_bucket >= fromUnixTimestamp64Milli(1739319154000) AND __hdx_time_bucket <= fromUnixTimestamp64Milli(1739491954000))`,
789+
},
790+
];
791+
792+
beforeEach(() => {
793+
mockMetadata.getColumn.mockImplementation(async ({ column }) =>
794+
column === 'date'
795+
? ({ type: 'Date' } as ColumnMeta)
796+
: ({ type: 'DateTime' } as ColumnMeta),
797+
);
798+
});
799+
800+
it.each(testCases)(
801+
'should generate a time filter expression $description',
802+
async ({
803+
timestampValueExpression,
804+
dateRangeEndInclusive = true,
805+
dateRangeStartInclusive = true,
806+
dateRange,
807+
expected,
808+
includedDataInterval,
809+
tableName = 'target_table',
810+
databaseName = 'default',
811+
primaryKey,
812+
}) => {
813+
if (primaryKey) {
814+
mockMetadata.getTableMetadata.mockResolvedValue({
815+
primary_key: primaryKey,
816+
} as any);
817+
}
818+
819+
const actual = await timeFilterExpr({
820+
timestampValueExpression,
821+
dateRangeEndInclusive,
822+
dateRangeStartInclusive,
823+
dateRange,
824+
connectionId: 'test-connection',
825+
databaseName,
826+
tableName,
827+
metadata: mockMetadata,
828+
includedDataInterval,
829+
});
830+
831+
const actualSql = parameterizedQueryToSql(actual);
832+
expect(actualSql).toBe(expected);
833+
},
834+
);
835+
});
633836
});

0 commit comments

Comments
 (0)