diff --git a/.changeset/tidy-invalid-dates.md b/.changeset/tidy-invalid-dates.md new file mode 100644 index 0000000000..c861c9eae2 --- /dev/null +++ b/.changeset/tidy-invalid-dates.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/common-utils": patch +--- + +Skip metadata key/value rollup queries when the date range contains an Invalid Date instead of binding NaN timestamps as ClickHouse `Int64` params, which failed with `BAD_QUERY_PARAMETER` (457). \ No newline at end of file diff --git a/packages/app/src/timeQuery.ts b/packages/app/src/timeQuery.ts index d2b80446f8..ce35ce04c9 100644 --- a/packages/app/src/timeQuery.ts +++ b/packages/app/src/timeQuery.ts @@ -17,7 +17,7 @@ import { subMilliseconds, } from 'date-fns'; import { - parseAsFloat, + createParser, parseAsString, useQueryState, useQueryStates, @@ -408,10 +408,27 @@ const getRelativeInterval = (start: Date, end: Date): string | undefined => { return `Past ${durationStr}`; }; -// This needs to be a stable reference to prevent rerenders +// `Date`'s own valid range is roughly ±8.64e15ms. nuqs's parseAsFloat only +// rejects NaN, so a URL from/to like `1e400` parses as Infinity (not NaN) +// and survives, later producing `new Date(Infinity)` = Invalid Date, which +// flows into metadata rollup queries as a "nan" ClickHouse Int64 param +// (#2933). Reject anything outside Date's valid range at the source. +const MAX_VALID_EPOCH_MS = 8_640_000_000_000_000; + +const parseAsValidEpochMs = createParser({ + parse: (v) => { + const float = parseFloat(v); + if (!Number.isFinite(float) || Math.abs(float) > MAX_VALID_EPOCH_MS) { + return null; + } + return float; + }, + serialize: v => v.toString(), +}); + const timeRangeQueryStateMap = { - from: parseAsFloat, - to: parseAsFloat, + from: parseAsValidEpochMs, + to: parseAsValidEpochMs, }; export function useNewTimeQuery({ diff --git a/packages/common-utils/src/core/__tests__/metadata.test.ts b/packages/common-utils/src/core/__tests__/metadata.test.ts new file mode 100644 index 0000000000..c80a4ca577 --- /dev/null +++ b/packages/common-utils/src/core/__tests__/metadata.test.ts @@ -0,0 +1,47 @@ +import { Metadata, MetadataCache } from '@/core/metadata'; +import { isDateRangeValid } from '@/core/utils'; +import type { BaseClickhouseClient } from '@/clickhouse'; +const invalidDate = new Date('not-a-date'); + +describe('isDateRangeValid', () => { + it('accepts valid date ranges', () => { + expect( + isDateRangeValid([ + new Date('2026-08-01T00:00:00Z'), + new Date('2026-08-02T00:00:00Z'), + ]), + ).toBe(true); + }); + + it('rejects a range with an invalid start', () => { + expect(isDateRangeValid([invalidDate, new Date()])).toBe(false); + }); + + it('rejects a range with an invalid end', () => { + expect(isDateRangeValid([new Date(), invalidDate])).toBe(false); + }); +}); + +describe('getMapKeys date range guard', () => { + it('returns [] and never queries ClickHouse for an invalid date range', async () => { + const query = jest + .fn() + .mockRejectedValue(new Error('should not be called')); + const metadata = new Metadata( + query as unknown as BaseClickhouseClient, + new MetadataCache(), + ); + + const keys = await metadata.getMapKeys({ + databaseName: 'db', + tableName: 'tbl', + column: 'attributes', + connectionId: 'conn', + metadataMVs: { granularity: 'minute' } as any, + dateRange: [invalidDate, invalidDate], + }); + + expect(keys).toEqual([]); + expect(query).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/common-utils/src/core/metadata.ts b/packages/common-utils/src/core/metadata.ts index d9e96e35e0..a7fbadc227 100644 --- a/packages/common-utils/src/core/metadata.ts +++ b/packages/common-utils/src/core/metadata.ts @@ -45,6 +45,7 @@ import { import { getAlignedDateRange, getDistributedTableArgs, + isDateRangeValid, MetadataMVQueryOptions, objectHash, TextIndexColumnQueryOptions, @@ -662,6 +663,15 @@ export class Metadata { signal?: AbortSignal; }) { inlineNonNegativeInt(maxKeys, 'maxKeys'); + if (dateRange && !isDateRangeValid(dateRange)) { + console.warn( + `getMapKeys: skipping metadata queries, dateRange contains an invalid date. ` + + `start=${dateRange[0]?.toString()} end=${dateRange[1]?.toString()} ` + + `databaseName=${databaseName} tableName=${tableName} column=${column} connectionId=${connectionId}`, + new Error('getMapKeys invalid date range').stack, + ); + return []; + } // Align date range to rollup granularity for consistent cache keys const alignedDateRange = @@ -1336,6 +1346,15 @@ export class Metadata { maxValuesPerKey: number; signal?: AbortSignal; }): Promise { + if (!isDateRangeValid(dateRange)) { + console.warn( + `getMetadataMVKeyValues: skipping rollup query, dateRange contains an invalid date. ` + + `start=${dateRange[0]?.toString()} end=${dateRange[1]?.toString()} ` + + `databaseName=${databaseName} connectionId=${connectionId}`, + new Error('getMetadataMVKeyValues invalid date range').stack, + ); + return undefined; + } const queryOptionsHash = objectHash(queryOptions); const metadataMVsHash = objectHash(metadataMVs ?? {}); const cacheKey = `${databaseName}.${connectionId}.${dateRange[0].toString()}.${dateRange[1].toString()}.${maxValuesPerKey}.${metadataMVsHash}.${queryOptionsHash}.getMetadataMVKeyValues`; diff --git a/packages/common-utils/src/core/utils.ts b/packages/common-utils/src/core/utils.ts index b117dd2661..9c5aba7ce4 100644 --- a/packages/common-utils/src/core/utils.ts +++ b/packages/common-utils/src/core/utils.ts @@ -1078,7 +1078,12 @@ export function getAlignedDateRange( return [alignedStart, alignedEnd]; } - +export function isDateRangeValid(dateRange: [Date, Date]): boolean { + return ( + Number.isFinite(dateRange[0].getTime()) && + Number.isFinite(dateRange[1].getTime()) + ); +} export function isDateRangeEqual(range1: [Date, Date], range2: [Date, Date]) { return ( range1[0].getTime() === range2[0].getTime() &&