From b5ebff2e6edbc5b465ca45d1bbdf8125b561d93b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 23 Jun 2026 14:54:46 +0000 Subject: [PATCH] fix(app): hydrate source fields for raw SQL tiles in useQueriedChartConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raw SQL tiles referencing source-dependent macros (, ) failed with 'Macro requires a source to be selected' when rendered outside the Dashboard Tile component (e.g. in notebook tiles), because the saved config only stores the source ID — the runtime fields (from, metricTables, etc.) were not merged before query execution. Add hydrateRawSqlConfigFromSource() to useQueriedChartConfig and useRenderedSqlChartConfig hooks. This mirrors the hydration pattern already established in DBDashboardPage's Tile useEffect, ensuring macros resolve correctly in all rendering contexts. Fixes HDX-4594 Co-authored-by: Drew Davis --- .changeset/fix-raw-sql-source-hydration.md | 13 ++++ packages/app/src/hooks/useChartConfig.tsx | 72 +++++++++++++++++----- 2 files changed, 70 insertions(+), 15 deletions(-) create mode 100644 .changeset/fix-raw-sql-source-hydration.md diff --git a/.changeset/fix-raw-sql-source-hydration.md b/.changeset/fix-raw-sql-source-hydration.md new file mode 100644 index 0000000000..c1f554e000 --- /dev/null +++ b/.changeset/fix-raw-sql-source-hydration.md @@ -0,0 +1,13 @@ +--- +'@hyperdx/app': patch +--- + +fix: hydrate source metadata for raw SQL tiles in all chart rendering contexts + +Raw SQL tiles referencing `$__sourceTable` or `$__filters` macros now correctly +resolve the source's table metadata regardless of the rendering context +(dashboards, notebooks, chart explorer). Previously, only the Dashboard Tile +component hydrated the `from`, `metricTables`, and other source-dependent fields +before query execution; other contexts passed the saved config directly, causing +macro expansion to fail with "Macro '$__sourceTable' requires a source to be +selected" despite the source being configured. diff --git a/packages/app/src/hooks/useChartConfig.tsx b/packages/app/src/hooks/useChartConfig.tsx index e2cb3c3c14..419f2bc775 100644 --- a/packages/app/src/hooks/useChartConfig.tsx +++ b/packages/app/src/hooks/useChartConfig.tsx @@ -1,3 +1,4 @@ +import { pick } from 'lodash'; import { chSqlToAliasMap, ClickHouseQueryError, @@ -22,7 +23,11 @@ import { BuilderChartConfigWithOptDateRange, ChartConfigWithDateRange, ChartConfigWithOptDateRange, + getSampleWeightExpression, + isLogSource, + isMetricSource, QuerySettings, + TSource, } from '@hyperdx/common-utils/dist/types'; import { useQuery, @@ -41,6 +46,35 @@ import { generateTimeWindowsDescending } from '@/utils/searchWindows'; import { useMVOptimizationExplanation } from './useMVOptimizationExplanation'; +/** + * Hydrate a raw SQL chart config with source metadata fields required for + * macro expansion ($__sourceTable, $__filters). The saved config only stores + * the source ID; the runtime fields (from, metricTables, etc.) must be merged + * from the fetched Source document before the config is rendered. + * + * Mirrors the hydration done in the Dashboard Tile useEffect + * (DBDashboardPage.tsx) so that all chart rendering contexts — dashboards, + * notebooks, chart explorer — resolve macros identically. + */ +export function hydrateRawSqlConfigFromSource< + T extends ChartConfigWithOptDateRange, +>(config: T, source: TSource | undefined): T { + if (!isRawSqlChartConfig(config) || !config.source || !source) { + return config; + } + return { + ...config, + ...pick(source, [ + 'implicitColumnExpression', + 'useTextIndexForImplicitColumn', + 'from', + ]), + ...(isLogSource(source) ? { bodyExpression: source.bodyExpression } : {}), + metricTables: isMetricSource(source) ? source.metricTables : undefined, + sampleWeightExpression: getSampleWeightExpression(source), + }; +} + interface AdditionalUseQueriedChartConfigOptions { onError?: (error: Error | ClickHouseQueryError) => void; /** @@ -295,11 +329,13 @@ export function useQueriedChartConfig( id: config.source, }); + const hydratedConfig = hydrateRawSqlConfigFromSource(config, source); + const query = useQuery({ // Include enableQueryChunking in the query key to ensure that queries with the // same config but different enableQueryChunking values do not share a query queryKey: [ - config, + hydratedConfig, options?.enableQueryChunking ?? false, options?.enableParallelQueries ?? false, ], @@ -307,14 +343,17 @@ export function useQueriedChartConfig( // https://tanstack.com/query/latest/docs/reference/streamedQuery queryFn: async context => { // PromQL queries go through the Prometheus API route, not ClickHouse proxy - if (isPromqlChartConfig(config) && config.dateRange) { - const [startDate, endDate] = config.dateRange; + if (isPromqlChartConfig(hydratedConfig) && hydratedConfig.dateRange) { + const [startDate, endDate] = hydratedConfig.dateRange; const startSec = startDate.getTime() / 1000; const endSec = endDate.getTime() / 1000; // Convert HyperDX granularity ("5 minute") to Prometheus step ("300s") let stepStr = '60s'; - if (config.granularity && config.granularity !== 'auto') { + if ( + hydratedConfig.granularity && + hydratedConfig.granularity !== 'auto' + ) { const granToSec: Record = { '15 second': 15, '30 second': 30, @@ -329,17 +368,17 @@ export function useQueriedChartConfig( '12 hour': 43200, '1 day': 86400, }; - stepStr = `${granToSec[config.granularity] ?? 60}s`; + stepStr = `${granToSec[hydratedConfig.granularity] ?? 60}s`; } const resp = await prometheusApi.queryRange({ - query: config.promqlExpression, + query: hydratedConfig.promqlExpression, start: startSec, end: endSec, step: stepStr, - connectionId: config.connection, - database: config.from?.databaseName ?? 'default', - table: config.from?.tableName ?? 'otel_metrics_ts', + connectionId: hydratedConfig.connection, + database: hydratedConfig.from?.databaseName ?? 'default', + table: hydratedConfig.from?.tableName ?? 'otel_metrics_ts', }); if (resp.status !== 'success' || !resp.data) { @@ -394,7 +433,8 @@ export function useQueriedChartConfig( }; } - const optimizedConfig = mvOptimizationData?.optimizedConfig ?? config; + const optimizedConfig = + mvOptimizationData?.optimizedConfig ?? hydratedConfig; const query = queryClient .getQueryCache() .find({ queryKey: context.queryKey, exact: true }); @@ -477,18 +517,20 @@ export function useRenderedSqlChartConfig( id: config.source, }); + const hydratedConfig = hydrateRawSqlConfigFromSource(config, source); + const query = useQuery({ - queryKey: ['renderedSql', config], + queryKey: ['renderedSql', hydratedConfig], queryFn: async () => { - const optimizedConfig = mvOptimizationData?.optimizedConfig ?? config; + const optimizedConfig = + mvOptimizationData?.optimizedConfig ?? hydratedConfig; const query = await renderChartConfig( optimizedConfig, metadata, source?.querySettings, ); const sql = parameterizedQueryToSql(query); - // sql-formatter can't handle prometheusQuery() / CTE syntax in PromQL queries - if (isPromqlChartConfig(config)) { + if (isPromqlChartConfig(hydratedConfig)) { return sql; } return format(sql); @@ -498,7 +540,7 @@ export function useRenderedSqlChartConfig( enabled && !isLoadingMVOptimization && !isSourceLoading && - !isPromqlChartConfig(config), + !isPromqlChartConfig(hydratedConfig), }); return {