diff --git a/.changeset/row-panel-materialized-alias-columns.md b/.changeset/row-panel-materialized-alias-columns.md
new file mode 100644
index 0000000000..d14af7b81d
--- /dev/null
+++ b/.changeset/row-panel-materialized-alias-columns.md
@@ -0,0 +1,15 @@
+---
+'@hyperdx/app': patch
+---
+
+feat: add an option to show MATERIALIZED and ALIAS columns in the row details panel
+
+ClickHouse leaves MATERIALIZED and ALIAS columns out of `SELECT *`, so the row
+details panel never showed them. A new "Show materialized and alias columns"
+item in the properties view options menu, off by default, adds
+`asterisk_include_materialized_columns` and `asterisk_include_alias_columns` to
+the row query. It has no effect on a source with a Known Columns List, and a
+value that the source's query settings give for either setting wins. If the row
+fails to load while the option is on (for example, because the connection's user
+is `readonly = 1`, or an ALIAS column cannot be evaluated), the error state
+offers to turn the option off.
diff --git a/packages/app/src/components/DBRowDataPanel.tsx b/packages/app/src/components/DBRowDataPanel.tsx
index 6a60cf5c0d..8540e06c5a 100644
--- a/packages/app/src/components/DBRowDataPanel.tsx
+++ b/packages/app/src/components/DBRowDataPanel.tsx
@@ -10,6 +10,11 @@ import {
import { Box } from '@mantine/core';
import { useQueriedChartConfig } from '@/hooks/useChartConfig';
+import {
+ getKnownColumnsList,
+ getSelectAllColumnsQuerySettings,
+ useSelectAllColumnsQuerySettings,
+} from '@/hooks/useMaterializedAliasColumnsOption';
import { WithClause } from '@/hooks/useRowWhere';
import {
getDisplayedTimestampValueExpression,
@@ -90,10 +95,7 @@ export function useRowData({
// target tables declare different column sets. When the source declares a
// "known columns" list (columns known to exist across all target tables) we
// select that instead of `*` when fetching full row data.
- const knownColumns =
- isLogSource(source) || isTraceSource(source)
- ? source.knownColumnsListExpression?.trim()
- : undefined;
+ const knownColumns = getKnownColumnsList(source);
const baseConfig = {
source: source.id,
@@ -210,6 +212,8 @@ export function useRowData({
...(aliasWith && aliasWith.length > 0 ? { with: aliasWith } : {}),
};
+ const additionalQuerySettings = useSelectAllColumnsQuerySettings(source);
+
const baseQueryKey = ['row_side_panel', rowId, aliasWith, source];
// Both halves of the filter are needed for `renderChartConfig` to emit one, so
// a source with no usable timestamp expression can't be bounded at all.
@@ -225,6 +229,7 @@ export function useRowData({
{
queryKey: [...baseQueryKey, dateRange],
enabled: rowId != null && hasWindow,
+ additionalQuerySettings,
},
);
@@ -245,6 +250,7 @@ export function useRowData({
const fallbackResult = useQueriedChartConfig(baseConfig, {
queryKey: [...baseQueryKey, undefined],
enabled: rowId != null && isFallbackActive,
+ additionalQuerySettings,
});
const queryResult = isFallbackActive ? fallbackResult : boundedResult;
@@ -379,6 +385,9 @@ export function RowDataPanel({
data={firstRow}
jsonColumns={jsonColumns}
mapColumns={mapColumns}
+ showMaterializedAliasColumnsOption={
+ getSelectAllColumnsQuerySettings(source) != null
+ }
/>
diff --git a/packages/app/src/components/DBRowJsonViewer.test.tsx b/packages/app/src/components/DBRowJsonViewer.test.tsx
index 32ef25dca2..4fc26a7a32 100644
--- a/packages/app/src/components/DBRowJsonViewer.test.tsx
+++ b/packages/app/src/components/DBRowJsonViewer.test.tsx
@@ -1,6 +1,8 @@
import React from 'react';
import { fireEvent, screen, within } from '@testing-library/react';
+import { useMaterializedAliasColumnsOption } from '@/hooks/useMaterializedAliasColumnsOption';
+
import { buildJSONExtractQuery, DBRowJsonViewer } from './DBRowJsonViewer';
import { RowSidePanelContext } from './DBRowSidePanel';
@@ -186,6 +188,46 @@ describe('DBRowJsonViewer', () => {
expect(mockToggleColumn).toHaveBeenCalledWith("LogAttributes['field1']");
});
+ it('turns the materialized and alias columns option on and off from the menu', async () => {
+ const OptionValue = () => (
+
+ {String(useMaterializedAliasColumnsOption()[0])}
+
+ );
+ const { container } = renderWithMantine(
+
+
+
+ ,
+ );
+ const option = screen.getByTestId('materialized-alias-option');
+ const toggle = async () => {
+ fireEvent.click(container.querySelector('[aria-haspopup="menu"]')!);
+ fireEvent.click(
+ await screen.findByTestId('json-viewer-materialized-alias-toggle'),
+ );
+ };
+
+ expect(option).toHaveTextContent('false');
+ await toggle();
+ expect(option).toHaveTextContent('true');
+ expect(
+ localStorage.getItem('hdx-row-show-materialized-alias-columns'),
+ ).toBe('true');
+ await toggle();
+ expect(option).toHaveTextContent('false');
+ });
+
+ it('hides the materialized and alias columns option unless asked to show it', async () => {
+ const { container } = renderComponent(logData);
+ fireEvent.click(container.querySelector('[aria-haspopup="menu"]')!);
+
+ expect(await screen.findByText('Hide blank values')).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('json-viewer-materialized-alias-toggle'),
+ ).not.toBeInTheDocument();
+ });
+
describe('timestamp fields', () => {
it('displays Timestamp using the same formatter as the results table', () => {
renderComponent({
diff --git a/packages/app/src/components/DBRowJsonViewer.tsx b/packages/app/src/components/DBRowJsonViewer.tsx
index f8e2ec2eaf..70a3ae6ada 100644
--- a/packages/app/src/components/DBRowJsonViewer.tsx
+++ b/packages/app/src/components/DBRowJsonViewer.tsx
@@ -32,6 +32,7 @@ import HyperJson, {
GetLineActions,
LineAction,
} from '@/components/HyperJson';
+import { useMaterializedAliasColumnsOption } from '@/hooks/useMaterializedAliasColumnsOption';
import { useFormatTime } from '@/useFormatTime';
import { isColumnInSelect, mergePath } from '@/utils';
import {
@@ -227,8 +228,16 @@ const viewerOptionsAtom = atomWithStorage(
viewerOptionsStorage,
);
-function HyperJsonMenu({ rowData }: { rowData: any }) {
+function HyperJsonMenu({
+ rowData,
+ showMaterializedAliasColumnsOption,
+}: {
+ rowData: any;
+ showMaterializedAliasColumnsOption: boolean;
+}) {
const [jsonOptions, setJsonOptions] = useAtom(viewerOptionsAtom);
+ const [showMaterializedAliasColumns, setShowMaterializedAliasColumns] =
+ useMaterializedAliasColumnsOption();
const effectiveWhiteSpace = jsonOptions.whiteSpace ?? 'pre-wrap';
return (
@@ -335,6 +344,23 @@ function HyperJsonMenu({ rowData }: { rowData: any }) {
>
Hide blank values
+ {showMaterializedAliasColumnsOption && (
+
+ ) : null
+ }
+ onClick={() =>
+ setShowMaterializedAliasColumns(!showMaterializedAliasColumns)
+ }
+ >
+ Show materialized and alias columns
+
+ )}
@@ -345,6 +371,7 @@ export function DBRowJsonViewer({
data,
jsonColumns,
mapColumns,
+ showMaterializedAliasColumnsOption = false,
}: {
data: any;
jsonColumns?: string[];
@@ -352,6 +379,9 @@ export function DBRowJsonViewer({
// `mergePath` so numeric-looking sub-keys on a Map render as
// `Map['key']` instead of the array `Map[N+1]`. HDX-4369.
mapColumns?: string[];
+ // Only a viewer of the whole row from `useRowData` is affected by the option,
+ // and only when the source's row query would change.
+ showMaterializedAliasColumnsOption?: boolean;
}) {
const formatTime = useFormatTime();
const {
@@ -699,7 +729,12 @@ export function DBRowJsonViewer({
)}
-
+
diff --git a/packages/app/src/components/DBRowSidePanelErrorState.tsx b/packages/app/src/components/DBRowSidePanelErrorState.tsx
index 19ed40599b..673df7879e 100644
--- a/packages/app/src/components/DBRowSidePanelErrorState.tsx
+++ b/packages/app/src/components/DBRowSidePanelErrorState.tsx
@@ -14,6 +14,10 @@ import { useDisclosure } from '@mantine/hooks';
import { IconAlertTriangle } from '@tabler/icons-react';
import { IS_LOCAL_MODE } from '@/config';
+import {
+ useMaterializedAliasColumnsOption,
+ useSelectAllColumnsQuerySettings,
+} from '@/hooks/useMaterializedAliasColumnsOption';
import { useTableMetadata } from '@/hooks/useMetadata';
import { useBrandDisplayName } from '@/theme/ThemeProvider';
@@ -82,6 +86,30 @@ function KnownColumnsListHint({
);
}
+// The option lives in the Column Values menu, which a failed row does not show.
+function MaterializedAliasColumnsHint({ onHide }: { onHide: () => void }) {
+ return (
+ }
+ title="Materialized and alias columns are on"
+ data-testid="materialized-alias-columns-hint"
+ >
+
+
+ The row query includes MATERIALIZED and ALIAS columns. The whole row
+ fails to load if the connection's user cannot change query
+ settings (for example, a readonly user) or if one of these columns
+ cannot be evaluated.
+
+
+
+
+ );
+}
+
export function DBRowSidePanelErrorState({
error,
source,
@@ -94,6 +122,9 @@ export function DBRowSidePanelErrorState({
const showHint =
isMissingColumnError(error) && !!tableMetadata?.isPointerTable;
+ const selectAllColumnsSettings = useSelectAllColumnsQuerySettings(source);
+ const [, setShowMaterializedAliasColumns] =
+ useMaterializedAliasColumnsOption();
return (
@@ -103,6 +134,12 @@ export function DBRowSidePanelErrorState({
)}
+ {selectAllColumnsSettings != null && (
+ setShowMaterializedAliasColumns(false)}
+ />
+ )}
+
Error Message:
diff --git a/packages/app/src/components/__tests__/DBRowDataPanel.test.ts b/packages/app/src/components/__tests__/DBRowDataPanel.test.ts
index 66152de309..94a5de0e2e 100644
--- a/packages/app/src/components/__tests__/DBRowDataPanel.test.ts
+++ b/packages/app/src/components/__tests__/DBRowDataPanel.test.ts
@@ -1,3 +1,4 @@
+import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse';
import { SourceKind, TLogSource } from '@hyperdx/common-utils/dist/types';
import { renderHook } from '@testing-library/react';
@@ -13,6 +14,8 @@ jest.mock('@/hooks/useChartConfig', () => ({
}));
const mockUseQueriedChartConfig = useQueriedChartConfig as jest.Mock;
+const showMaterializedAliasColumns = () =>
+ localStorage.setItem('hdx-row-show-materialized-alias-columns', 'true');
describe('DBRowDataPanel', () => {
const source: TLogSource = {
@@ -28,6 +31,7 @@ describe('DBRowDataPanel', () => {
beforeEach(() => {
jest.clearAllMocks();
+ localStorage.clear();
mockUseQueriedChartConfig.mockReturnValue({
data: {
data: [],
@@ -93,6 +97,114 @@ describe('DBRowDataPanel', () => {
expect(config.select).not.toContainEqual({ valueExpression: '*' });
});
+ it('adds no query settings while the viewer option is off', () => {
+ renderHook(() => useRowData({ source, rowId: "id='abc123'" }));
+
+ const [bounded, fallback] = mockUseQueriedChartConfig.mock.calls;
+ for (const [, options] of [bounded, fallback]) {
+ expect(options.additionalQuerySettings).toBeUndefined();
+ expect(options.retry).toBeUndefined();
+ }
+ });
+
+ it('includes MATERIALIZED and ALIAS columns when the viewer option is on', () => {
+ showMaterializedAliasColumns();
+ renderHook(() => useRowData({ source, rowId: "id='abc123'" }));
+
+ const [bounded, fallback] = mockUseQueriedChartConfig.mock.calls;
+ for (const [, options] of [bounded, fallback]) {
+ expect(options.additionalQuerySettings).toEqual([
+ { setting: 'asterisk_include_materialized_columns', value: '1' },
+ { setting: 'asterisk_include_alias_columns', value: '1' },
+ ]);
+ }
+ });
+
+ it('adds only the settings that the source does not set itself', () => {
+ showMaterializedAliasColumns();
+ const sourceWithSetting: TLogSource = {
+ ...source,
+ querySettings: [
+ { setting: 'asterisk_include_alias_columns', value: '0' },
+ ],
+ };
+
+ renderHook(() =>
+ useRowData({ source: sourceWithSetting, rowId: "id='abc123'" }),
+ );
+
+ const [[, options]] = mockUseQueriedChartConfig.mock.calls;
+ expect(options.additionalQuerySettings).toEqual([
+ { setting: 'asterisk_include_materialized_columns', value: '1' },
+ ]);
+ });
+
+ it('adds no query settings when the source sets both itself', () => {
+ showMaterializedAliasColumns();
+ const sourceWithSettings: TLogSource = {
+ ...source,
+ querySettings: [
+ { setting: 'asterisk_include_materialized_columns', value: '0' },
+ { setting: 'asterisk_include_alias_columns', value: '0' },
+ ],
+ };
+
+ renderHook(() =>
+ useRowData({ source: sourceWithSettings, rowId: "id='abc123'" }),
+ );
+
+ const [[, options]] = mockUseQueriedChartConfig.mock.calls;
+ expect(options.additionalQuerySettings).toBeUndefined();
+ });
+
+ it('adds no query settings when the source has a Known Columns List', () => {
+ showMaterializedAliasColumns();
+ const sourceWithKnownColumns: TLogSource = {
+ ...source,
+ knownColumnsListExpression: 'Timestamp, Body, ServiceName',
+ };
+
+ renderHook(() =>
+ useRowData({ source: sourceWithKnownColumns, rowId: "id='abc123'" }),
+ );
+
+ const [bounded, fallback] = mockUseQueriedChartConfig.mock.calls;
+ for (const [, options] of [bounded, fallback]) {
+ expect(options.additionalQuerySettings).toBeUndefined();
+ }
+ });
+
+ it('reports the error and does not drop the settings when the connection rejects them', () => {
+ showMaterializedAliasColumns();
+ // The client's parsed error, wrapped the way the row query sees it.
+ const message =
+ "Cannot modify 'asterisk_include_materialized_columns' setting in readonly mode. ";
+ const readonlyError = new ClickHouseQueryError(message, 'SELECT * LIMIT 1');
+ readonlyError.cause = Object.assign(new Error(message), {
+ code: '164',
+ type: 'READONLY',
+ });
+ mockUseQueriedChartConfig.mockReturnValue({
+ data: undefined,
+ error: readonlyError,
+ isLoading: false,
+ isPending: false,
+ isError: true,
+ isSuccess: false,
+ });
+
+ const { result } = renderHook(() =>
+ useRowData({ source, rowId: "id='abc123'" }),
+ );
+
+ expect(result.current.isError).toBe(true);
+ expect(result.current.error).toBe(readonlyError);
+ expect(result.current.isLoading).toBe(false);
+ for (const [, options] of mockUseQueriedChartConfig.mock.calls) {
+ expect(options.additionalQuerySettings).toBeDefined();
+ }
+ });
+
describe('time filtering', () => {
// A row id synthesized from ids alone (e.g. "View Trace" builds
// TraceId + SpanId) has no timestamp of its own, so without a dateRange the
diff --git a/packages/app/src/components/__tests__/DBRowSidePanelErrorState.test.tsx b/packages/app/src/components/__tests__/DBRowSidePanelErrorState.test.tsx
index f67795ff90..9e06654849 100644
--- a/packages/app/src/components/__tests__/DBRowSidePanelErrorState.test.tsx
+++ b/packages/app/src/components/__tests__/DBRowSidePanelErrorState.test.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse';
import { SourceKind, TLogSource } from '@hyperdx/common-utils/dist/types';
import { MantineProvider } from '@mantine/core';
-import { render, screen } from '@testing-library/react';
+import { fireEvent, render, screen } from '@testing-library/react';
import { DBRowSidePanelErrorState } from '@/components/DBRowSidePanelErrorState';
@@ -129,4 +129,41 @@ describe('DBRowSidePanelErrorState', () => {
),
).toBeNull();
});
+
+ describe('materialized and alias columns hint', () => {
+ const OPTION_KEY = 'hdx-row-show-materialized-alias-columns';
+ // The hint does not depend on the error, so any error shows it.
+ const error = new ClickHouseQueryError(
+ 'Timeout exceeded: elapsed 30 seconds',
+ 'SELECT * FROM logs LIMIT 1',
+ );
+
+ beforeEach(() => {
+ localStorage.clear();
+ });
+
+ it('offers to hide the columns while the option is on', () => {
+ localStorage.setItem(OPTION_KEY, 'true');
+ renderErrorState(error);
+
+ fireEvent.click(
+ screen.getByRole('button', {
+ name: 'Hide materialized and alias columns',
+ }),
+ );
+
+ expect(localStorage.getItem(OPTION_KEY)).toBe('false');
+ expect(
+ screen.queryByTestId('materialized-alias-columns-hint'),
+ ).toBeNull();
+ });
+
+ it('is not shown while the option is off', () => {
+ renderErrorState(error);
+
+ expect(
+ screen.queryByTestId('materialized-alias-columns-hint'),
+ ).toBeNull();
+ });
+ });
});
diff --git a/packages/app/src/hooks/__tests__/useChartConfig.test.tsx b/packages/app/src/hooks/__tests__/useChartConfig.test.tsx
index 0793af11e0..a94296ca30 100644
--- a/packages/app/src/hooks/__tests__/useChartConfig.test.tsx
+++ b/packages/app/src/hooks/__tests__/useChartConfig.test.tsx
@@ -21,6 +21,7 @@ import {
appendChunk,
getGranularityAlignedTimeWindows,
getMinGranularitySeconds,
+ mergeQuerySettings,
useQueriedChartConfig,
} from '@/hooks/useChartConfig';
import { useMVOptimizationExplanation } from '@/hooks/useMVOptimizationExplanation';
@@ -1155,6 +1156,73 @@ describe('useChartConfig', () => {
expect(result.current.isPending).toBe(false);
});
+ it('passes additionalQuerySettings to the query', async () => {
+ const config = createMockChartConfig({
+ dateRange: undefined,
+ granularity: undefined,
+ });
+ const additionalQuerySettings = [
+ { setting: 'asterisk_include_alias_columns', value: '1' },
+ ];
+
+ mockClickhouseClient.queryChartConfig.mockResolvedValue(
+ createMockQueryResponse([]),
+ );
+
+ const { result } = renderHook(
+ () => useQueriedChartConfig(config, { additionalQuerySettings }),
+ {
+ wrapper,
+ },
+ );
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+
+ expect(mockClickhouseClient.queryChartConfig).toHaveBeenCalledTimes(1);
+ expect(mockClickhouseClient.queryChartConfig).toHaveBeenCalledWith({
+ config,
+ metadata: expect.any(Object),
+ opts: {
+ abort_signal: expect.any(AbortSignal),
+ },
+ querySettings: additionalQuerySettings,
+ });
+ });
+
+ it('adds additionalQuerySettings to a queryKey that the caller passes', async () => {
+ const config = createMockChartConfig({
+ dateRange: undefined,
+ granularity: undefined,
+ });
+ const additionalQuerySettings = [
+ { setting: 'asterisk_include_alias_columns', value: '1' },
+ ];
+
+ mockClickhouseClient.queryChartConfig.mockResolvedValue(
+ createMockQueryResponse([]),
+ );
+
+ const { result } = renderHook(
+ () =>
+ useQueriedChartConfig(config, {
+ queryKey: ['caller-key'],
+ additionalQuerySettings,
+ }),
+ {
+ wrapper,
+ },
+ );
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+
+ expect(
+ queryClient.getQueryCache().find({
+ queryKey: ['caller-key', additionalQuerySettings],
+ exact: true,
+ }),
+ ).toBeDefined();
+ });
+
it('fetches data without chunking when no granularity is provided', async () => {
const config = createMockChartConfig({
dateRange: [new Date('2025-10-01'), new Date('2025-10-02')],
@@ -2304,6 +2372,43 @@ describe('useChartConfig', () => {
});
});
+ describe('mergeQuerySettings', () => {
+ const sourceSettings = [{ setting: 'max_threads', value: '4' }];
+
+ it('returns the source settings unchanged when there is nothing to add', () => {
+ expect(mergeQuerySettings(sourceSettings, undefined)).toBe(
+ sourceSettings,
+ );
+ expect(mergeQuerySettings(undefined, [])).toBeUndefined();
+ });
+
+ it('adds the additional settings after the source settings', () => {
+ expect(
+ mergeQuerySettings(sourceSettings, [
+ { setting: 'asterisk_include_alias_columns', value: '1' },
+ ]),
+ ).toEqual([
+ { setting: 'max_threads', value: '4' },
+ { setting: 'asterisk_include_alias_columns', value: '1' },
+ ]);
+ });
+
+ it('adds only the settings that the source does not define', () => {
+ expect(
+ mergeQuerySettings(
+ [{ setting: 'asterisk_include_alias_columns', value: '0' }],
+ [
+ { setting: 'asterisk_include_materialized_columns', value: '1' },
+ { setting: 'asterisk_include_alias_columns', value: '1' },
+ ],
+ ),
+ ).toEqual([
+ { setting: 'asterisk_include_alias_columns', value: '0' },
+ { setting: 'asterisk_include_materialized_columns', value: '1' },
+ ]);
+ });
+ });
+
describe('appendChunk', () => {
const empty = { data: [], meta: [], rows: 0, isComplete: false };
diff --git a/packages/app/src/hooks/useChartConfig.tsx b/packages/app/src/hooks/useChartConfig.tsx
index ea96708564..6cd74bda85 100644
--- a/packages/app/src/hooks/useChartConfig.tsx
+++ b/packages/app/src/hooks/useChartConfig.tsx
@@ -72,6 +72,12 @@ interface AdditionalUseQueriedChartConfigOptions {
* replaces the key the hook builds from the config.
*/
queryKeyPrefix?: string;
+ /**
+ * Query settings for this query only, added after the source's own query
+ * settings. A setting that the source already defines keeps its value.
+ * They are added to the query key, also when the caller passes `queryKey`.
+ */
+ additionalQuerySettings?: QuerySettings;
}
type TimeWindow = {
@@ -284,6 +290,28 @@ async function* fetchDataInChunks({
}
}
+/**
+ * Adds per-query settings after the source's query settings. A setting that the
+ * source already defines keeps the source's value.
+ */
+export function mergeQuerySettings(
+ sourceSettings: QuerySettings | undefined,
+ additionalSettings: QuerySettings | undefined,
+): QuerySettings | undefined {
+ if (!additionalSettings?.length) {
+ return sourceSettings;
+ }
+ const sourceSettingNames = new Set(
+ (sourceSettings ?? []).map(({ setting }) => setting),
+ );
+ return [
+ ...(sourceSettings ?? []),
+ ...additionalSettings.filter(
+ ({ setting }) => !sourceSettingNames.has(setting),
+ ),
+ ];
+}
+
/** Append the given chunk to the given accumulated result. Exported for tests. */
export function appendChunk(
accumulated: TQueryFnData,
@@ -371,6 +399,9 @@ export function useQueriedChartConfig(
options?.enableQueryChunking ?? false,
options?.enableParallelQueries ?? false,
minGranularitySeconds,
+ ...(options?.additionalQuerySettings?.length
+ ? [options.additionalQuerySettings]
+ : []),
],
// TODO: Replace this with `streamedQuery` when it is no longer experimental. Use 'replace' refetch mode.
// https://tanstack.com/query/latest/docs/reference/streamedQuery
@@ -403,7 +434,10 @@ export function useQueriedChartConfig(
enableQueryChunking: options?.enableQueryChunking,
enableParallelQueries: options?.enableParallelQueries,
metadata,
- querySettings: source?.querySettings,
+ querySettings: mergeQuerySettings(
+ source?.querySettings,
+ options?.additionalQuerySettings,
+ ),
});
let accumulatedChunks: TQueryFnData = emptyValue;
@@ -437,6 +471,9 @@ export function useQueriedChartConfig(
retry: 1,
refetchOnWindowFocus: false,
...options,
+ ...(options?.queryKey && options.additionalQuerySettings?.length
+ ? { queryKey: [...options.queryKey, options.additionalQuerySettings] }
+ : {}),
enabled: enabled && !isLoadingMVOptimization && !isSourceLoading,
});
diff --git a/packages/app/src/hooks/useMaterializedAliasColumnsOption.ts b/packages/app/src/hooks/useMaterializedAliasColumnsOption.ts
new file mode 100644
index 0000000000..ba70435379
--- /dev/null
+++ b/packages/app/src/hooks/useMaterializedAliasColumnsOption.ts
@@ -0,0 +1,53 @@
+import {
+ isLogSource,
+ isTraceSource,
+ QuerySettings,
+ TSource,
+} from '@hyperdx/common-utils/dist/types';
+
+import { useLocalStorage } from '@/utils';
+
+// ClickHouse leaves MATERIALIZED and ALIAS columns out of `SELECT *` unless
+// these settings are on.
+const SELECT_ALL_COLUMNS_QUERY_SETTINGS: QuerySettings = [
+ { setting: 'asterisk_include_materialized_columns', value: '1' },
+ { setting: 'asterisk_include_alias_columns', value: '1' },
+];
+
+export function useMaterializedAliasColumnsOption() {
+ return useLocalStorage('hdx-row-show-materialized-alias-columns', false);
+}
+
+// A Known Columns List replaces `SELECT *` in the row query.
+export function getKnownColumnsList(source: TSource): string | undefined {
+ return isLogSource(source) || isTraceSource(source)
+ ? source.knownColumnsListExpression?.trim() || undefined
+ : undefined;
+}
+
+// The settings that the row query adds for this source while the option is on,
+// if any. A setting that the source's own query settings define keeps the
+// source's value.
+export function getSelectAllColumnsQuerySettings(
+ source: TSource,
+): QuerySettings | undefined {
+ if (getKnownColumnsList(source)) {
+ return undefined;
+ }
+ const added = SELECT_ALL_COLUMNS_QUERY_SETTINGS.filter(
+ ({ setting }) =>
+ !source.querySettings?.some(
+ sourceSetting => sourceSetting.setting === setting,
+ ),
+ );
+ return added.length > 0 ? added : undefined;
+}
+
+export function useSelectAllColumnsQuerySettings(
+ source: TSource,
+): QuerySettings | undefined {
+ const [showMaterializedAliasColumns] = useMaterializedAliasColumnsOption();
+ return showMaterializedAliasColumns
+ ? getSelectAllColumnsQuerySettings(source)
+ : undefined;
+}
diff --git a/packages/common-utils/src/__tests__/queryChartConfig.int.test.ts b/packages/common-utils/src/__tests__/queryChartConfig.int.test.ts
index 892b24a59a..427f7f6f76 100644
--- a/packages/common-utils/src/__tests__/queryChartConfig.int.test.ts
+++ b/packages/common-utils/src/__tests__/queryChartConfig.int.test.ts
@@ -2856,4 +2856,72 @@ describe('queryChartConfig Integration Tests', () => {
expect(Number(col(rows[0], 'Error rate'))).toBeCloseTo(40, 5);
});
});
+
+ // The row details panel selects `*` with these settings (see useRowData).
+ describe('SELECT * with MATERIALIZED and ALIAS columns', () => {
+ const ROW_TABLE = 'select_all_columns_int_test';
+ const config: ChartConfigWithOptDateRange = {
+ connection: 'test-connection',
+ from: { databaseName: DATABASE, tableName: ROW_TABLE },
+ select: [{ valueExpression: '*' }],
+ where: 'Id = 1',
+ limit: { limit: 1 },
+ };
+
+ beforeAll(async () => {
+ await client.command({
+ query: `CREATE OR REPLACE TABLE ${DATABASE}.${ROW_TABLE} (
+ Id UInt32,
+ StatusCode UInt16,
+ StatusClass String MATERIALIZED if(StatusCode >= 500, '5xx', 'other'),
+ IsServerError Bool ALIAS StatusCode >= 500
+ )
+ ENGINE = MergeTree
+ ORDER BY Id`,
+ });
+ await client.insert({
+ table: `${DATABASE}.${ROW_TABLE}`,
+ values: [{ Id: 1, StatusCode: 503 }],
+ format: 'JSONEachRow',
+ });
+ });
+
+ afterAll(async () => {
+ await client.command({
+ query: `DROP TABLE IF EXISTS ${DATABASE}.${ROW_TABLE}`,
+ });
+ });
+
+ it('leaves MATERIALIZED and ALIAS columns out of `*` by default', async () => {
+ const result = await hdxClient.queryChartConfig({
+ config,
+ metadata,
+ querySettings: undefined,
+ });
+
+ expect(result.meta?.map(m => m.name)).toEqual(['Id', 'StatusCode']);
+ });
+
+ it('returns MATERIALIZED and ALIAS columns with the asterisk_include_* settings', async () => {
+ const result = await hdxClient.queryChartConfig({
+ config,
+ metadata,
+ querySettings: [
+ { setting: 'asterisk_include_materialized_columns', value: '1' },
+ { setting: 'asterisk_include_alias_columns', value: '1' },
+ ],
+ });
+
+ expect(result.meta?.map(m => m.name)).toEqual([
+ 'Id',
+ 'StatusCode',
+ 'StatusClass',
+ 'IsServerError',
+ ]);
+ expect(result.data[0]).toMatchObject({
+ StatusClass: '5xx',
+ IsServerError: true,
+ });
+ });
+ });
});