Skip to content
Draft
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
5 changes: 4 additions & 1 deletion packages/app/src/components/DBSearchPageFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2079,7 +2079,10 @@ const DBSearchPageFiltersComponent = ({
);
};

function isFieldPrimary(tableMetadata: TableMetadata | undefined, key: string) {
function isFieldPrimary(
tableMetadata: TableMetadata | null | undefined,
key: string,
) {
return tableMetadata?.primary_key?.includes(key);
}
export const DBSearchPageFilters = memo(DBSearchPageFiltersComponent);
50 changes: 40 additions & 10 deletions packages/app/src/components/SearchInput/SearchInputV2.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useController, UseControllerProps } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import {
Field,
TableConnection,
TableConnectionChoice,
} from '@hyperdx/common-utils/dist/core/metadata';
import { genEnglishExplanation } from '@hyperdx/common-utils/dist/queryParser';
Expand Down Expand Up @@ -32,6 +33,12 @@ export class LuceneLanguageFormatter implements ILanguageFormatter {
}

const luceneLanguageFormatter = new LuceneLanguageFormatter();

function stableTableConnectionToKey(tc: TableConnection | undefined): string {
if (!tc) return '';
return `${tc.connectionId}|${tc.databaseName}|${tc.tableName}`;
}

export default function SearchInputV2({
tableConnection,
tableConnections,
Expand Down Expand Up @@ -87,17 +94,40 @@ export default function SearchInputV2({
},
);

// Callers commonly pass an inline `tcFromSource(source)` for `tableConnection`,
// which produces a fresh object reference on every parent render. Without
// stabilizing here, the explanation effect below would re-run continuously,
// re-triggering schema queries and (when the underlying ClickHouse database
// doesn't exist) setting the same string state in a tight loop, eventually
// tripping React's "Maximum update depth exceeded" guard.
const stableTableConnectionKey = stableTableConnectionToKey(tableConnection);
const stableTableConnection = useMemo<TableConnection | undefined>(
() => tableConnection,
// eslint-disable-next-line react-hooks/exhaustive-deps
[stableTableConnectionKey],
);

useEffect(() => {
if (tableConnection) {
genEnglishExplanation({
query: value,
tableConnection,
metadata,
}).then(q => {
setParsedEnglishQuery(q);
if (!stableTableConnection) return;
let cancelled = false;
genEnglishExplanation({
query: value,
tableConnection: stableTableConnection,
metadata,
})
.then(q => {
if (!cancelled) setParsedEnglishQuery(q);
})
.catch(err => {
// Schema lookups can fail for sources whose database/table no longer
// exists. Swallow these so a stale source doesn't surface as an
// unhandled rejection that contributes to render-loop crashes.
console.warn('Failed to compute lucene query explanation:', err);
});
}
}, [value, tableConnection, metadata]);
return () => {
cancelled = true;
};
}, [value, stableTableConnection, metadata]);

useHotkeys(
['/', 's'],
Expand Down
43 changes: 23 additions & 20 deletions packages/app/src/hooks/useFieldExpressionGenerator.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useCallback, useMemo } from 'react';
import SqlString from 'sqlstring';
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
import { TSource } from '@hyperdx/common-utils/dist/types';
Expand All @@ -24,24 +25,26 @@ export default function useFieldExpressionGenerator(
tcFromSource(source),
);

if (source && !isLoadingJsonColumns) {
return {
isLoading: false,
getFieldExpression: (
column: string,
key: string,
convertFn: string = 'toString',
) => {
const isJson = jsonColumns?.includes(column);
return isJson
? SqlString.format(`${convertFn}(??.??)`, [column, key])
: SqlString.format('??[?]', [column, key]);
},
};
} else {
return {
isLoading: isLoadingJsonColumns,
getFieldExpression: undefined,
};
}
// Memoize the generator so it has a stable reference across renders when
// its inputs haven't changed. Several call sites pass the returned function
// into `useMemo` / `useEffect` dependency arrays, and a fresh closure on
// every render previously caused expensive recomputation cascades that
// amplified render-loop bugs when the underlying schema query failed.
const getFieldExpression = useCallback<FieldExpressionGenerator>(
(column, key, convertFn = 'toString') => {
const isJson = jsonColumns?.includes(column);
return isJson
? SqlString.format(`${convertFn}(??.??)`, [column, key])
: SqlString.format('??[?]', [column, key]);
},
[jsonColumns],
);

return useMemo(
() =>
source && !isLoadingJsonColumns
? { isLoading: false, getFieldExpression }
: { isLoading: isLoadingJsonColumns, getFieldExpression: undefined },
[source, isLoadingJsonColumns, getFieldExpression],
);
}
24 changes: 21 additions & 3 deletions packages/app/src/hooks/useMetadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ export function useMetadataWithSettings() {
return metadata;
}

// Schema-discovery queries (DESCRIBE / system.tables) deterministically fail
// when the source's database or table doesn't exist on the current ClickHouse
// server. There's nothing useful to gain from retrying those failures, and the
// retries amplify spurious re-renders that have caused render-loop crashes
// (e.g. "Maximum update depth exceeded") in pages that observe these hooks.
const SCHEMA_QUERY_RETRY = 0;

export function useColumns(
{
databaseName,
Expand All @@ -96,6 +103,7 @@ export function useColumns(
connectionId,
});
},
retry: SCHEMA_QUERY_RETRY,
enabled: !!databaseName && !!tableName && !!connectionId,
...options,
});
Expand All @@ -117,8 +125,9 @@ export function useJsonColumns(
) ?? []
);
},
retry: SCHEMA_QUERY_RETRY,
enabled:
tableConnection &&
!!tableConnection &&
!!tableConnection.databaseName &&
!!tableConnection.tableName &&
!!tableConnection.connectionId,
Expand Down Expand Up @@ -208,15 +217,24 @@ export function useTableMetadata(
options?: Omit<UseQueryOptions<any, Error>, 'queryKey'>,
) {
const metadata = useMetadataWithSettings();
return useQuery<TableMetadata | undefined>({
return useQuery<TableMetadata | null>({
queryKey: ['useMetadata.useTableMetadata', { databaseName, tableName }],
queryFn: async () => {
return await metadata.getTableMetadata({
// `getTableMetadata` resolves to `undefined` when the table doesn't
// exist (e.g. the source points at a database that's missing on the
// current ClickHouse server). React Query v5 forbids `undefined` query
// results and throws "Query data cannot be undefined", which previously
// surfaced as a hard render-loop crash from consumers of this hook.
// Normalize the missing case to `null` so the absence is still
// observable but the query resolves cleanly.
const tableMetadata = await metadata.getTableMetadata({
databaseName,
tableName,
connectionId,
});
return tableMetadata ?? null;
},
retry: SCHEMA_QUERY_RETRY,
staleTime: 1000 * 60 * 5, // Cache every 5 min
enabled: !!databaseName && !!tableName && !!connectionId,
...options,
Expand Down
Loading