This is an automated email from the ASF dual-hosted git repository. kgabryje pushed a commit to branch what-if in repository https://gitbox.apache.org/repos/asf/superset.git
commit 4eaf707aabf6450be238dd5b55d2dcd306ebf68a Author: Kamil Gabryjelski <[email protected]> AuthorDate: Thu Dec 18 14:25:53 2025 +0100 Handle sql expressions --- .../dashboard/components/WhatIfDrawer/index.tsx | 140 ++++++++------ .../util/charts/getFormDataWithExtraFilters.ts | 21 +- .../src/dashboard/util/useWhatIfHighlightStyles.ts | 25 ++- .../src/dashboard/util/whatIf.test.ts | 211 +++++++++++++++++++++ superset-frontend/src/dashboard/util/whatIf.ts | 156 ++++++++++++++- 5 files changed, 488 insertions(+), 65 deletions(-) diff --git a/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx b/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx index 3666af1b47..a381f60f01 100644 --- a/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx +++ b/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx @@ -25,6 +25,7 @@ import { Select, Checkbox, Tooltip, + Tag, } from '@superset-ui/core/components'; import Slider from '@superset-ui/core/components/Slider'; import { Icons } from '@superset-ui/core/components/Icons'; @@ -39,7 +40,7 @@ import WhatIfAIInsights from './WhatIfAIInsights'; import { fetchRelatedColumnSuggestions } from './whatIfApi'; import { ExtendedWhatIfModification } from './types'; -export const WHAT_IF_PANEL_WIDTH = 300; +export const WHAT_IF_PANEL_WIDTH = 340; const SLIDER_MIN = -50; const SLIDER_MAX = 50; @@ -102,7 +103,7 @@ const PanelContent = styled.div` padding: ${({ theme }) => theme.sizeUnit * 4}px; display: flex; flex-direction: column; - gap: ${({ theme }) => theme.sizeUnit * 4}px; + gap: ${({ theme }) => theme.sizeUnit * 5}px; `; const FormSection = styled.div` @@ -136,56 +137,56 @@ const CheckboxContainer = styled.div` const ModificationsSection = styled.div` display: flex; flex-direction: column; - gap: ${({ theme }) => theme.sizeUnit * 2}px; + gap: ${({ theme }) => theme.sizeUnit * 5}px; `; -const ModificationsSectionTitle = styled.div` - color: ${({ theme }) => theme.colorText}; - font-size: ${({ theme }) => theme.fontSizeSM}px; +const ModificationTagsContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: ${({ theme }) => theme.sizeUnit * 2}px; `; -const ModificationCard = styled.div<{ isAISuggested?: boolean }>` - padding: ${({ theme }) => theme.sizeUnit * 2}px; - background-color: ${({ theme, isAISuggested }) => - isAISuggested ? theme.colorInfoBg : theme.colorBgLayout}; - border: 1px solid - ${({ theme, isAISuggested }) => - isAISuggested ? theme.colorInfoBorder : theme.colorBorderSecondary}; - border-radius: ${({ theme }) => theme.borderRadius}px; +const AIBadge = styled.span` + font-size: 10px; + padding: 0 4px; + background-color: ${({ theme }) => theme.colorInfo}; + color: ${({ theme }) => theme.colorWhite}; + border-radius: 16px; + line-height: 1.2; `; -const ModificationHeader = styled.div` +const AIReasoningSection = styled.div` display: flex; - align-items: center; - justify-content: space-between; + flex-direction: column; gap: ${({ theme }) => theme.sizeUnit}px; `; -const ModificationColumn = styled.span` - font-weight: ${({ theme }) => theme.fontWeightStrong}; - color: ${({ theme }) => theme.colorText}; -`; +const AIReasoningToggle = styled.button` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.sizeUnit}px; + background: none; + border: none; + padding: 0; + cursor: pointer; + color: ${({ theme }) => theme.colorTextTertiary}; + font-size: ${({ theme }) => theme.fontSizeSM}px; -const ModificationValue = styled.span<{ isPositive: boolean }>` - font-weight: ${({ theme }) => theme.fontWeightStrong}; - color: ${({ theme, isPositive }) => - isPositive ? theme.colorSuccess : theme.colorError}; + &:hover { + color: ${({ theme }) => theme.colorText}; + } `; -const AIBadge = styled.span` - font-size: ${({ theme }) => theme.fontSizeXS}px; - padding: 2px 6px; - background-color: ${({ theme }) => theme.colorInfo}; - color: ${({ theme }) => theme.colorWhite}; - border-radius: ${({ theme }) => theme.borderRadius}px; - font-weight: ${({ theme }) => theme.fontWeightStrong}; +const AIReasoningContent = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.sizeUnit}px; + padding-left: ${({ theme }) => theme.sizeUnit * 4}px; `; -const ModificationReasoning = styled.div` +const AIReasoningItem = styled.div` font-size: ${({ theme }) => theme.fontSizeSM}px; color: ${({ theme }) => theme.colorTextSecondary}; - margin-top: ${({ theme }) => theme.sizeUnit}px; - font-style: italic; `; interface WhatIfPanelProps { @@ -207,6 +208,7 @@ const WhatIfPanel = ({ onClose, topOffset }: WhatIfPanelProps) => { >([]); // Counter that increments each time Apply is clicked, used as key to reset AI insights const [applyCounter, setApplyCounter] = useState(0); + const [showAIReasoning, setShowAIReasoning] = useState(false); const slices = useSelector( (state: RootState) => state.sliceEntities.slices as { [id: number]: Slice }, @@ -458,31 +460,57 @@ const WhatIfPanel = ({ onClose, topOffset }: WhatIfPanelProps) => { {appliedModifications.length > 0 && ( <ModificationsSection> - <ModificationsSectionTitle> - {t('Applied modifications')} - </ModificationsSectionTitle> - {appliedModifications.map((mod, idx) => ( - <ModificationCard key={idx} isAISuggested={mod.isAISuggested}> - <ModificationHeader> - <ModificationColumn>{mod.column}</ModificationColumn> - <div + <ModificationTagsContainer> + {appliedModifications.map((mod, idx) => ( + <Tag + key={idx} + css={css` + display: inline-flex; + align-items: center; + gap: ${theme.sizeUnit}px; + margin: 0; + `} + > + <span>{mod.column}</span> + {mod.isAISuggested && <AIBadge>{t('AI')}</AIBadge>} + <span css={css` - display: flex; - align-items: center; - gap: ${theme.sizeUnit}px; + font-weight: ${theme.fontWeightStrong}; + color: ${mod.multiplier >= 1 + ? theme.colorSuccess + : theme.colorError}; `} > - <ModificationValue isPositive={mod.multiplier >= 1}> - {formatPercentage(mod.multiplier)} - </ModificationValue> - {mod.isAISuggested && <AIBadge>{t('AI')}</AIBadge>} - </div> - </ModificationHeader> - {mod.reasoning && ( - <ModificationReasoning>{mod.reasoning}</ModificationReasoning> + {formatPercentage(mod.multiplier)} + </span> + </Tag> + ))} + </ModificationTagsContainer> + {appliedModifications.some(mod => mod.reasoning) && ( + <AIReasoningSection> + <AIReasoningToggle + onClick={() => setShowAIReasoning(!showAIReasoning)} + > + {showAIReasoning ? ( + <Icons.DownOutlined iconSize="xs" /> + ) : ( + <Icons.RightOutlined iconSize="xs" /> + )} + {t('How AI chose these')} + </AIReasoningToggle> + {showAIReasoning && ( + <AIReasoningContent> + {appliedModifications + .filter(mod => mod.reasoning) + .map((mod, idx) => ( + <AIReasoningItem key={idx}> + <strong>{mod.column}:</strong> {mod.reasoning} + </AIReasoningItem> + ))} + </AIReasoningContent> )} - </ModificationCard> - ))} + </AIReasoningSection> + )} </ModificationsSection> )} diff --git a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts index 9300eb2f9d..c8490632b9 100644 --- a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts +++ b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts @@ -31,6 +31,7 @@ import { ChartQueryPayload, ActiveFilters, WhatIfModification, + Slice, } from 'src/dashboard/types'; import { ChartCustomizationItem } from 'src/dashboard/components/nativeFilters/ChartCustomization/types'; import { getExtraFormData } from 'src/dashboard/components/nativeFilters/utils'; @@ -43,6 +44,10 @@ import { } from './chartTypeLimitations'; import getEffectiveExtraFilters from './getEffectiveExtraFilters'; import { getAllActiveFilters } from '../activeAllDashboardFilters'; +import { + collectSqlExpressionsFromSlice, + findColumnsInSqlExpressions, +} from '../whatIf'; interface CachedFormData { extra_form_data?: JsonObject; @@ -547,8 +552,20 @@ export default function getFormDataWithExtraFilters({ let whatIfExtras: { what_if?: { modifications: WhatIfModification[] } } = {}; if (whatIfModifications && whatIfModifications.length > 0) { const chartColumns = buildExistingColumnsSet(chart as ChartQueryPayload); - const applicableModifications = whatIfModifications.filter(mod => - chartColumns.has(mod.column), + + // Also check if modified columns appear in SQL expressions + // (e.g., custom metrics like AVG(orders / customers)) + const modifiedColumnNames = whatIfModifications.map(mod => mod.column); + const sliceForSqlCheck = { form_data: chart.form_data } as Slice; + const sqlExpressions = collectSqlExpressionsFromSlice(sliceForSqlCheck); + const sqlReferencedColumns = findColumnsInSqlExpressions( + sqlExpressions, + modifiedColumnNames, + ); + + const applicableModifications = whatIfModifications.filter( + mod => + chartColumns.has(mod.column) || sqlReferencedColumns.has(mod.column), ); if (applicableModifications.length > 0) { diff --git a/superset-frontend/src/dashboard/util/useWhatIfHighlightStyles.ts b/superset-frontend/src/dashboard/util/useWhatIfHighlightStyles.ts index 07182642fc..24150d914b 100644 --- a/superset-frontend/src/dashboard/util/useWhatIfHighlightStyles.ts +++ b/superset-frontend/src/dashboard/util/useWhatIfHighlightStyles.ts @@ -20,7 +20,11 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { css, keyframes } from '@emotion/react'; import { RootState, WhatIfModification } from 'src/dashboard/types'; -import { extractColumnsFromSlice } from './whatIf'; +import { + extractColumnsFromSlice, + collectSqlExpressionsFromSlice, + findColumnsInSqlExpressions, +} from './whatIf'; const EMPTY_STYLES = undefined; @@ -119,17 +123,30 @@ const useWhatIfHighlightStyles = (chartId: number) => { } const chartColumns = extractColumnsFromSlice(slice); - const modifiedColumns = new Set( - whatIfModifications.map((mod: WhatIfModification) => mod.column), + const modifiedColumnNames = whatIfModifications.map( + (mod: WhatIfModification) => mod.column, ); + const modifiedColumns = new Set(modifiedColumnNames); - // Check if any of the chart's columns are being modified + // Check if any of the chart's explicitly referenced columns are being modified for (const column of chartColumns) { if (modifiedColumns.has(column)) { return true; } } + // Also check if modified columns appear in SQL expressions + const sqlExpressions = collectSqlExpressionsFromSlice(slice); + if (sqlExpressions.length > 0) { + const sqlReferencedColumns = findColumnsInSqlExpressions( + sqlExpressions, + modifiedColumnNames, + ); + if (sqlReferencedColumns.size > 0) { + return true; + } + } + return false; }, [whatIfModifications, slice]); diff --git a/superset-frontend/src/dashboard/util/whatIf.test.ts b/superset-frontend/src/dashboard/util/whatIf.test.ts new file mode 100644 index 0000000000..4a20e4cdb5 --- /dev/null +++ b/superset-frontend/src/dashboard/util/whatIf.test.ts @@ -0,0 +1,211 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + collectSqlExpressionsFromSlice, + findColumnsInSqlExpressions, + sliceUsesColumn, +} from './whatIf'; +import { Slice } from '../types'; + +const createMockSlice = (formData: Record<string, unknown>): Slice => + ({ + slice_id: 1, + slice_name: 'Test Slice', + form_data: formData, + }) as Slice; + +test('collectSqlExpressionsFromSlice extracts SQL from metrics', () => { + const slice = createMockSlice({ + metrics: [ + { + expressionType: 'SQL', + sqlExpression: 'AVG(orders / customers)', + label: 'Avg Orders', + }, + ], + }); + + const expressions = collectSqlExpressionsFromSlice(slice); + expect(expressions).toEqual(['AVG(orders / customers)']); +}); + +test('collectSqlExpressionsFromSlice extracts SQL from filters', () => { + const slice = createMockSlice({ + adhoc_filters: [ + { + expressionType: 'SQL', + sqlExpression: 'revenue > 1000', + clause: 'WHERE', + }, + ], + }); + + const expressions = collectSqlExpressionsFromSlice(slice); + expect(expressions).toEqual(['revenue > 1000']); +}); + +test('collectSqlExpressionsFromSlice extracts SQL from adhoc columns in groupby', () => { + const slice = createMockSlice({ + groupby: [ + { + sqlExpression: "DATE_TRUNC('month', order_date)", + label: 'Month', + }, + ], + }); + + const expressions = collectSqlExpressionsFromSlice(slice); + expect(expressions).toEqual(["DATE_TRUNC('month', order_date)"]); +}); + +test('collectSqlExpressionsFromSlice extracts SQL from singular metric', () => { + const slice = createMockSlice({ + metric: { + expressionType: 'SQL', + sqlExpression: 'SUM(amount)', + label: 'Total Amount', + }, + }); + + const expressions = collectSqlExpressionsFromSlice(slice); + expect(expressions).toEqual(['SUM(amount)']); +}); + +test('collectSqlExpressionsFromSlice ignores SIMPLE expression types', () => { + const slice = createMockSlice({ + metrics: [ + { + expressionType: 'SIMPLE', + column: { column_name: 'revenue' }, + aggregate: 'SUM', + }, + ], + adhoc_filters: [ + { + expressionType: 'SIMPLE', + subject: 'status', + operator: '==', + comparator: 'active', + }, + ], + }); + + const expressions = collectSqlExpressionsFromSlice(slice); + expect(expressions).toEqual([]); +}); + +test('findColumnsInSqlExpressions finds exact column matches', () => { + const sqlExpressions = ['AVG(orders / customers)', 'SUM(revenue)']; + const columnNames = ['orders', 'customers', 'revenue', 'total']; + + const found = findColumnsInSqlExpressions(sqlExpressions, columnNames); + expect(found).toEqual(new Set(['orders', 'customers', 'revenue'])); +}); + +test('findColumnsInSqlExpressions avoids false positives with similar names', () => { + const sqlExpressions = ['SUM(order_count)', 'AVG(reorder_rate)']; + const columnNames = ['order', 'orders', 'order_count', 'reorder_rate']; + + const found = findColumnsInSqlExpressions(sqlExpressions, columnNames); + // Should only match exact column names, not partial matches + expect(found).toEqual(new Set(['order_count', 'reorder_rate'])); + expect(found.has('order')).toBe(false); + expect(found.has('orders')).toBe(false); +}); + +test('findColumnsInSqlExpressions handles columns at start and end of expression', () => { + const sqlExpressions = ['revenue + cost']; + const columnNames = ['revenue', 'cost']; + + const found = findColumnsInSqlExpressions(sqlExpressions, columnNames); + expect(found).toEqual(new Set(['revenue', 'cost'])); +}); + +test('findColumnsInSqlExpressions handles columns in parentheses', () => { + const sqlExpressions = ['SUM(amount)', '(price * quantity)']; + const columnNames = ['amount', 'price', 'quantity']; + + const found = findColumnsInSqlExpressions(sqlExpressions, columnNames); + expect(found).toEqual(new Set(['amount', 'price', 'quantity'])); +}); + +test('findColumnsInSqlExpressions handles special regex characters in column names', () => { + const sqlExpressions = ['SUM(col.name) + AVG(col$value)']; + const columnNames = ['col.name', 'col$value']; + + const found = findColumnsInSqlExpressions(sqlExpressions, columnNames); + expect(found).toEqual(new Set(['col.name', 'col$value'])); +}); + +test('findColumnsInSqlExpressions returns empty set when no matches', () => { + const sqlExpressions = ['SUM(total)']; + const columnNames = ['revenue', 'cost']; + + const found = findColumnsInSqlExpressions(sqlExpressions, columnNames); + expect(found.size).toBe(0); +}); + +test('findColumnsInSqlExpressions returns empty set with empty inputs', () => { + expect(findColumnsInSqlExpressions([], ['col1']).size).toBe(0); + expect(findColumnsInSqlExpressions(['SUM(col)'], []).size).toBe(0); + expect(findColumnsInSqlExpressions([], []).size).toBe(0); +}); + +test('sliceUsesColumn detects columns in SQL expressions', () => { + const slice = createMockSlice({ + metrics: [ + { + expressionType: 'SQL', + sqlExpression: 'AVG(orders / customers)', + label: 'Avg', + }, + ], + }); + + expect(sliceUsesColumn(slice, 'orders')).toBe(true); + expect(sliceUsesColumn(slice, 'customers')).toBe(true); + expect(sliceUsesColumn(slice, 'revenue')).toBe(false); +}); + +test('sliceUsesColumn detects explicitly referenced columns', () => { + const slice = createMockSlice({ + groupby: ['category', 'region'], + }); + + expect(sliceUsesColumn(slice, 'category')).toBe(true); + expect(sliceUsesColumn(slice, 'region')).toBe(true); + expect(sliceUsesColumn(slice, 'country')).toBe(false); +}); + +test('sliceUsesColumn detects columns in both explicit and SQL references', () => { + const slice = createMockSlice({ + groupby: ['category'], + metrics: [ + { + expressionType: 'SQL', + sqlExpression: 'SUM(revenue)', + label: 'Total', + }, + ], + }); + + expect(sliceUsesColumn(slice, 'category')).toBe(true); + expect(sliceUsesColumn(slice, 'revenue')).toBe(true); +}); diff --git a/superset-frontend/src/dashboard/util/whatIf.ts b/superset-frontend/src/dashboard/util/whatIf.ts index cfe590899f..caed76f04b 100644 --- a/superset-frontend/src/dashboard/util/whatIf.ts +++ b/superset-frontend/src/dashboard/util/whatIf.ts @@ -28,6 +28,124 @@ export function isNumericColumn(column: ColumnMeta): boolean { return column.type_generic === GenericDataType.Numeric; } +/** + * Collect all SQL expressions from a slice's form_data. + * This includes: + * - Metrics with expressionType: 'SQL' (sqlExpression) + * - Filters with expressionType: 'SQL' (sqlExpression) + * - Adhoc columns in groupby, x_axis, series, etc. (sqlExpression) + */ +export function collectSqlExpressionsFromSlice(slice: Slice): string[] { + const expressions: string[] = []; + const formData = slice.form_data; + if (!formData) return expressions; + + // Helper to extract sqlExpression from adhoc columns + const addAdhocColumnExpression = (col: unknown) => { + if ( + col && + typeof col === 'object' && + 'sqlExpression' in col && + typeof (col as { sqlExpression: unknown }).sqlExpression === 'string' + ) { + expressions.push((col as { sqlExpression: string }).sqlExpression); + } + }; + + // Extract SQL expressions from metrics + ensureIsArray(formData.metrics).forEach((metric: unknown) => { + if ( + metric && + typeof metric === 'object' && + 'expressionType' in metric && + (metric as { expressionType: unknown }).expressionType === 'SQL' && + 'sqlExpression' in metric && + typeof (metric as { sqlExpression: unknown }).sqlExpression === 'string' + ) { + expressions.push((metric as { sqlExpression: string }).sqlExpression); + } + }); + + // Extract SQL expression from singular metric + if ( + formData.metric && + typeof formData.metric === 'object' && + 'expressionType' in formData.metric && + (formData.metric as { expressionType: unknown }).expressionType === 'SQL' && + 'sqlExpression' in formData.metric && + typeof (formData.metric as { sqlExpression: unknown }).sqlExpression === + 'string' + ) { + expressions.push( + (formData.metric as { sqlExpression: string }).sqlExpression, + ); + } + + // Extract SQL expressions from filters + ensureIsArray(formData.adhoc_filters).forEach((filter: unknown) => { + if ( + filter && + typeof filter === 'object' && + 'expressionType' in filter && + (filter as { expressionType: unknown }).expressionType === 'SQL' && + 'sqlExpression' in filter && + typeof (filter as { sqlExpression: unknown }).sqlExpression === 'string' + ) { + expressions.push((filter as { sqlExpression: string }).sqlExpression); + } + }); + + // Extract SQL expressions from adhoc columns in groupby, x_axis, series, columns, entity + ensureIsArray(formData.groupby).forEach(addAdhocColumnExpression); + ensureIsArray(formData.columns).forEach(addAdhocColumnExpression); + + if (formData.x_axis) { + addAdhocColumnExpression(formData.x_axis); + } + if (formData.series) { + addAdhocColumnExpression(formData.series); + } + if (formData.entity) { + addAdhocColumnExpression(formData.entity); + } + + return expressions; +} + +/** + * Find column names that appear in SQL expressions. + * Uses word boundary matching to avoid false positives + * (e.g., "order" shouldn't match "order_id" or "reorder"). + */ +export function findColumnsInSqlExpressions( + sqlExpressions: string[], + columnNames: string[], +): Set<string> { + const foundColumns = new Set<string>(); + + if (sqlExpressions.length === 0 || columnNames.length === 0) { + return foundColumns; + } + + // Combine all SQL expressions into one string for efficient searching + const combinedSql = sqlExpressions.join(' '); + + columnNames.forEach(columnName => { + // Use word boundary regex to match exact column names + // Escape special regex characters in column name + const escapedName = columnName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // Match column name surrounded by word boundaries or common SQL delimiters + const regex = new RegExp( + `(^|[^a-zA-Z0-9_])${escapedName}([^a-zA-Z0-9_]|$)`, + ); + if (regex.test(combinedSql)) { + foundColumns.add(columnName); + } + }); + + return foundColumns; +} + /** * Extract column names from a slice's form_data * This includes columns from groupby, metrics, x_axis, series, filters, etc. @@ -136,9 +254,27 @@ export function getNumericColumnsForDashboard( const datasource = datasources[datasourceKey]; if (!datasource?.columns) return; - // Extract columns referenced by this slice + // Extract columns explicitly referenced by this slice const referencedColumns = extractColumnsFromSlice(slice); + // Also check SQL expressions for column references + const sqlExpressions = collectSqlExpressionsFromSlice(slice); + if (sqlExpressions.length > 0) { + // Get all numeric column names from this datasource + const numericColumnNames = datasource.columns + .filter((c: ColumnMeta) => isNumericColumn(c)) + .map((c: ColumnMeta) => c.column_name); + + // Find which numeric columns are referenced in SQL expressions + const sqlReferencedColumns = findColumnsInSqlExpressions( + sqlExpressions, + numericColumnNames, + ); + + // Add SQL-referenced columns to the set + sqlReferencedColumns.forEach(colName => referencedColumns.add(colName)); + } + // For each referenced column, check if it's numeric referencedColumns.forEach(colName => { const colMetadata = datasource.columns.find( @@ -171,9 +307,23 @@ export function getNumericColumnsForDashboard( } /** - * Check if a slice uses a specific column + * Check if a slice uses a specific column. + * Checks both explicitly referenced columns and columns in SQL expressions. */ export function sliceUsesColumn(slice: Slice, columnName: string): boolean { const columns = extractColumnsFromSlice(slice); - return columns.has(columnName); + if (columns.has(columnName)) { + return true; + } + + // Also check SQL expressions + const sqlExpressions = collectSqlExpressionsFromSlice(slice); + if (sqlExpressions.length > 0) { + const sqlReferencedColumns = findColumnsInSqlExpressions(sqlExpressions, [ + columnName, + ]); + return sqlReferencedColumns.has(columnName); + } + + return false; }
