Repository navigation
fix: v2 search drops rows after a sub-second startTime; make maxResults cap configurable #3381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| '@hyperdx/common-utils': patch | ||
| '@hyperdx/api': patch | ||
| --- | ||
|
|
||
| fix: Floor the time filter start to the precision of a coarse timestamp column (DateTime, DateTime64 below ms) when a finer column also filters the range, so a sub-second start no longer drops the rest of that tick; make the external API v2 search `maxResults` cap configurable via `EXTERNAL_API_SEARCH_MAX_RESULTS` (defaults to 2000, the previous hardcoded value) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2650,7 +2650,9 @@ describe('renderChartConfig', () => { | |
| mockMetadata.getColumn.mockImplementation(async ({ column }) => | ||
| column === 'date' | ||
| ? ({ type: 'Date' } as ColumnMeta) | ||
| : ({ type: 'DateTime' } as ColumnMeta), | ||
| : column === 'TimestampTime' | ||
| ? ({ type: 'DateTime' } as ColumnMeta) | ||
| : ({ type: 'DateTime64(3)' } as ColumnMeta), | ||
| ); | ||
| }); | ||
|
|
||
|
|
@@ -2759,6 +2761,187 @@ describe('renderChartConfig', () => { | |
| ); | ||
| }); | ||
|
|
||
| it.each([ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — Sub-second fix and configurable cap are only checked as SQL strings, never against ClickHouse or the route Add a case to Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The integration logs table only has a full-precision |
||
| ['inclusive', true], | ||
| ['exclusive', false], | ||
| ])( | ||
| 'floors a sub-second %s start on a DateTime column and keeps it inclusive', | ||
| async (_, dateRangeStartInclusive) => { | ||
| const start = new Date('2026-10-13T22:03:21.827Z'); | ||
| const end = new Date('2026-10-13T22:05:00.500Z'); | ||
|
|
||
| const actual = await timeFilterExpr({ | ||
| timestampValueExpression: 'TimestampTime, Timestamp', | ||
| dateRangeEndInclusive: false, | ||
| dateRangeStartInclusive, | ||
| dateRange: [start, end], | ||
| connectionId: 'test-connection', | ||
| databaseName: 'default', | ||
| tableName: 'target_table', | ||
| metadata: mockMetadata, | ||
| }); | ||
|
|
||
| const precise = dateRangeStartInclusive ? '>=' : '>'; | ||
| expect(parameterizedQueryToSql(actual)).toBe( | ||
| `(TimestampTime >= fromUnixTimestamp64Milli(${new Date('2026-10-13T22:03:21Z').getTime()}) AND TimestampTime < fromUnixTimestamp64Milli(${end.getTime()}))AND(Timestamp ${precise} fromUnixTimestamp64Milli(${start.getTime()}) AND Timestamp < fromUnixTimestamp64Milli(${end.getTime()}))`, | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| it.each([ | ||
| ['inclusive', true, '>='], | ||
| ['exclusive', false, '>'], | ||
| ])( | ||
| 'leaves a %s start unfloored on a DateTime-only source so adjacent windows do not overlap', | ||
| async (_, dateRangeStartInclusive, op) => { | ||
| const start = new Date('2026-10-13T22:03:21.827Z'); | ||
| const end = new Date('2026-10-13T22:05:00Z'); | ||
|
|
||
| const actual = await timeFilterExpr({ | ||
| timestampValueExpression: 'TimestampTime', | ||
| dateRangeEndInclusive: true, | ||
| dateRangeStartInclusive, | ||
| dateRange: [start, end], | ||
| connectionId: 'test-connection', | ||
| databaseName: 'default', | ||
| tableName: 'target_table', | ||
| metadata: mockMetadata, | ||
| }); | ||
|
|
||
| expect(parameterizedQueryToSql(actual)).toBe( | ||
| `(TimestampTime ${op} fromUnixTimestamp64Milli(${start.getTime()}) AND TimestampTime <= fromUnixTimestamp64Milli(${end.getTime()}))`, | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| it.each([ | ||
| ['DateTime', 'toDateTime($__fromTime_ms)'], | ||
| ['DateTime64(1)', 'toDateTime64($__fromTime_ms, 1)'], | ||
| ])( | ||
| 'floors the start macro on a %s column in a raw SQL template', | ||
| async (type, expectedStart) => { | ||
| mockMetadata.getColumn.mockImplementation(async ({ column }) => | ||
| column === 'coarse' | ||
| ? ({ type } as ColumnMeta) | ||
| : ({ type: 'DateTime64(9)' } as ColumnMeta), | ||
| ); | ||
|
|
||
| const actual = await timeFilterExpr({ | ||
| timestampValueExpression: 'coarse, Timestamp', | ||
| dateRangeEndInclusive: true, | ||
| dateRangeStartInclusive: true, | ||
| dateRange: [ | ||
| new Date('2026-10-13T22:03:21.827Z'), | ||
| new Date('2026-10-13T22:05:00Z'), | ||
| ], | ||
| isRenderingRawSqlTemplate: true, | ||
| connectionId: 'test-connection', | ||
| databaseName: 'default', | ||
| tableName: 'target_table', | ||
| metadata: mockMetadata, | ||
| }); | ||
|
|
||
| expect(parameterizedQueryToSql(actual)).toBe( | ||
| `(coarse >= ${expectedStart} AND coarse <= $__toTime_ms)AND(Timestamp >= $__fromTime_ms AND Timestamp <= $__toTime_ms)`, | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| it.each([ | ||
| ['DateTime', '22:03:21.000'], | ||
| ["DateTime('UTC')", '22:03:21.000'], | ||
| ['Nullable(DateTime)', '22:03:21.000'], | ||
| ['DateTime64(0)', '22:03:21.000'], | ||
| ["DateTime64(0, 'UTC')", '22:03:21.000'], | ||
| ['DateTime64(1)', '22:03:21.800'], | ||
| ['DateTime64(2)', '22:03:21.820'], | ||
| ['DateTime64(3)', '22:03:21.827'], | ||
| ])( | ||
| 'floors the start bound of a %s column to %s when a finer column exists', | ||
| async (type, flooredStart) => { | ||
| mockMetadata.getColumn.mockImplementation(async ({ column }) => | ||
| column === 'coarse' | ||
| ? ({ type } as ColumnMeta) | ||
| : ({ type: 'DateTime64(9)' } as ColumnMeta), | ||
| ); | ||
| const start = new Date('2026-10-13T22:03:21.827Z'); | ||
| const end = new Date('2026-10-13T22:05:00Z'); | ||
|
|
||
| const actual = await timeFilterExpr({ | ||
| timestampValueExpression: 'coarse, Timestamp', | ||
| dateRangeEndInclusive: true, | ||
| dateRangeStartInclusive: true, | ||
| dateRange: [start, end], | ||
| connectionId: 'test-connection', | ||
| databaseName: 'default', | ||
| tableName: 'target_table', | ||
| metadata: mockMetadata, | ||
| }); | ||
|
|
||
| expect(parameterizedQueryToSql(actual)).toBe( | ||
| `(coarse >= fromUnixTimestamp64Milli(${new Date(`2026-10-13T${flooredStart}Z`).getTime()}) AND coarse <= fromUnixTimestamp64Milli(${end.getTime()}))AND(Timestamp >= fromUnixTimestamp64Milli(${start.getTime()}) AND Timestamp <= fromUnixTimestamp64Milli(${end.getTime()}))`, | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| describe('sub-second start flooring edge cases', () => { | ||
| const start = new Date('2026-10-13T22:03:21.827Z'); | ||
| const flooredStart = new Date('2026-10-13T22:03:21Z'); | ||
| const end = new Date('2026-10-13T22:05:00Z'); | ||
|
|
||
| const render = ( | ||
| timestampValueExpression: string, | ||
| includedDataInterval?: string, | ||
| ) => | ||
| timeFilterExpr({ | ||
| timestampValueExpression, | ||
| dateRangeEndInclusive: true, | ||
| dateRangeStartInclusive: false, | ||
| dateRange: [start, end], | ||
| connectionId: 'test-connection', | ||
| databaseName: 'default', | ||
| tableName: 'target_table', | ||
| metadata: mockMetadata, | ||
| includedDataInterval, | ||
| }).then(parameterizedQueryToSql); | ||
|
|
||
| it('does not floor when the finer column type cannot be resolved', async () => { | ||
| mockMetadata.getColumn.mockImplementation(async ({ column }) => | ||
| column === 'TimestampTime' | ||
| ? ({ type: 'DateTime' } as ColumnMeta) | ||
| : undefined, | ||
| ); | ||
|
|
||
| expect(await render('TimestampTime, Timestamp')).toBe( | ||
| `(TimestampTime > fromUnixTimestamp64Milli(${start.getTime()}) AND TimestampTime <= fromUnixTimestamp64Milli(${end.getTime()}))AND(Timestamp > fromUnixTimestamp64Milli(${start.getTime()}) AND Timestamp <= fromUnixTimestamp64Milli(${end.getTime()}))`, | ||
| ); | ||
| }); | ||
|
|
||
| it('does not treat a toStartOf column as the finer column', async () => { | ||
| expect(await render('toStartOfMinute(Timestamp), TimestampTime')).toBe( | ||
| `(toStartOfMinute(Timestamp) >= toStartOfMinute(fromUnixTimestamp64Milli(${start.getTime()})) AND toStartOfMinute(Timestamp) <= toStartOfMinute(fromUnixTimestamp64Milli(${end.getTime()})))AND(TimestampTime > fromUnixTimestamp64Milli(${start.getTime()}) AND TimestampTime <= fromUnixTimestamp64Milli(${end.getTime()}))`, | ||
| ); | ||
| }); | ||
|
|
||
| it('floors the start inside the includedDataInterval expansion', async () => { | ||
| expect(await render('TimestampTime, Timestamp', '1 minute')).toBe( | ||
| `(TimestampTime >= toStartOfInterval(fromUnixTimestamp64Milli(${flooredStart.getTime()}), INTERVAL 1 minute) - INTERVAL 1 minute AND TimestampTime <= toStartOfInterval(fromUnixTimestamp64Milli(${end.getTime()}), INTERVAL 1 minute) + INTERVAL 1 minute)AND(Timestamp > toStartOfInterval(fromUnixTimestamp64Milli(${start.getTime()}), INTERVAL 1 minute) - INTERVAL 1 minute AND Timestamp <= toStartOfInterval(fromUnixTimestamp64Milli(${end.getTime()}), INTERVAL 1 minute) + INTERVAL 1 minute)`, | ||
| ); | ||
| }); | ||
|
|
||
| it('treats a Nullable(DateTime64(9)) column as the finer column', async () => { | ||
| mockMetadata.getColumn.mockImplementation(async ({ column }) => | ||
| column === 'TimestampTime' | ||
| ? ({ type: 'DateTime' } as ColumnMeta) | ||
| : ({ type: 'Nullable(DateTime64(9))' } as ColumnMeta), | ||
| ); | ||
|
|
||
| expect(await render('TimestampTime, Timestamp')).toBe( | ||
| `(TimestampTime >= fromUnixTimestamp64Milli(${flooredStart.getTime()}) AND TimestampTime <= fromUnixTimestamp64Milli(${end.getTime()}))AND(Timestamp > fromUnixTimestamp64Milli(${start.getTime()}) AND Timestamp <= fromUnixTimestamp64Milli(${end.getTime()}))`, | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| it.each(['Date32', 'Nullable(Date)', 'LowCardinality(Date)'])( | ||
| 'wraps a %s column in toDate() and keeps the bounds inclusive', | ||
| async type => { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.