This is an automated email from the ASF dual-hosted git repository.

EnxDev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 3f77b342cb8 fix(explore): persist aliased column config by real key 
(#44955)
3f77b342cb8 is described below

commit 3f77b342cb84d2ec0a5563addc73e3091f47cc43
Author: Amogh Atreya <[email protected]>
AuthorDate: Fri Oct 23 00:05:05 2026 -0700

    fix(explore): persist aliased column config by real key (#44955)
    
    Co-authored-by: Cursor <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../src/utils/headerGroups.test.ts                 | 182 ++++++++++++++++++++-
 .../src/utils/headerGroups.ts                      |  68 +++++++-
 .../src/controlPanel.tsx                           |   5 +
 .../src/transformProps.ts                          |   9 +-
 .../src/utils/isEqualColumns.ts                    |   8 +
 .../test/controlPanel.test.tsx                     |  34 ++++
 .../test/transformProps.test.ts                    | 159 ++++++++++++++++++
 .../src/Radar/controlPanel.tsx                     |  10 +-
 .../plugin-chart-table/src/controlPanel.tsx        |   5 +
 .../plugin-chart-table/src/transformProps.ts       |   9 +-
 .../plugin-chart-table/src/utils/isEqualColumns.ts |   8 +
 .../plugin-chart-table/test/TableChart.test.tsx    | 112 +++++++++++++
 .../plugin-chart-table/test/controlPanel.test.tsx  |  34 ++++
 .../ColumnConfigControl.test.tsx                   |  75 +++++++++
 .../ColumnConfigControl/ColumnConfigControl.tsx    |  20 ++-
 .../controls/ColumnConfigControl/types.ts          |   1 +
 16 files changed, 722 insertions(+), 17 deletions(-)

diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/headerGroups.test.ts
 
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/headerGroups.test.ts
index 91c3f9b7255..e982972d19e 100644
--- 
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/headerGroups.test.ts
+++ 
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/headerGroups.test.ts
@@ -16,7 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ComparisonType, QueryMode } from '@superset-ui/core';
+import { ComparisonType, DTTM_ALIAS, QueryMode } from '@superset-ui/core';
+import { COLUMN_NAME_ALIASES } from '../constants';
 import {
   buildHeaderGroupRows,
   buildTimeComparisonHeaderGroups,
@@ -29,6 +30,7 @@ import {
   isHeaderGroupsTimeComparisonEnabled,
   nestColDefsInHeaderGroups,
   normalizeColumnConfigKeys,
+  getKnownColumnConfigKeys,
   resolveHeaderGroups,
   syncTimeComparisonGroups,
   toStoredTimeComparisonColumnKey,
@@ -377,6 +379,29 @@ test('toStoredTimeComparisonColumnKey leaves unmatched 
comparison-like keys unch
   ).toBe('# profit');
 });
 
+test('normalizeColumnConfigKeys leaves real result columns alone when a 
sibling looks like a comparison slot', () => {
+  expect(
+    normalizeColumnConfigKeys(
+      { 'Previous revenue': { customColumnName: 'Prior' } },
+      ['Main revenue', 'Previous revenue', '# revenue'],
+    ),
+  ).toEqual({
+    'Previous revenue': { customColumnName: 'Prior' },
+  });
+});
+
+test('normalizeColumnConfigKeys does not treat a known dataset column as a 
comparison slot', () => {
+  expect(
+    normalizeColumnConfigKeys(
+      { 'Previous revenue': { visible: false } },
+      ['Main revenue', '# revenue'],
+      ['Previous revenue', 'Main revenue', '# revenue'],
+    ),
+  ).toEqual({
+    'Previous revenue': { visible: false },
+  });
+});
+
 test('normalizeColumnConfigKeys remaps localized Main keys onto stored 
colnames', () => {
   expect(
     normalizeColumnConfigKeys(
@@ -393,6 +418,9 @@ test('normalizeColumnConfigKeys remaps localized Main keys 
onto stored colnames'
 });
 
 test('normalizeColumnConfigKeys prefers an existing stored Main entry over a 
localized duplicate', () => {
+  const expected = {
+    'Main revenue': { columnWidth: 120 },
+  };
   expect(
     normalizeColumnConfigKeys(
       {
@@ -401,9 +429,16 @@ test('normalizeColumnConfigKeys prefers an existing stored 
Main entry over a loc
       },
       comparisonRevenueColumns,
     ),
-  ).toEqual({
-    'Main revenue': { columnWidth: 120 },
-  });
+  ).toEqual(expected);
+  expect(
+    normalizeColumnConfigKeys(
+      {
+        'Principale revenue': { columnWidth: 300 },
+        'Main revenue': { columnWidth: 120 },
+      },
+      comparisonRevenueColumns,
+    ),
+  ).toEqual(expected);
 });
 
 test('normalizeColumnConfigKeys returns an empty object without saved config', 
() => {
@@ -422,6 +457,145 @@ test('normalizeColumnConfigKeys keeps keys that do not 
match colnames', () => {
   ).toEqual({ leftover: { columnWidth: 10 } });
 });
 
+test('getKnownColumnConfigKeys includes colnames, dataset columns, and 
metrics', () => {
+  expect(
+    getKnownColumnConfigKeys(['__timestamp'], {
+      columns: [{ column_name: 'Time' }, { column_name: 'region' }],
+      metrics: [{ metric_name: 'count' }],
+    }),
+  ).toEqual(['__timestamp', 'Time', 'region', 'count']);
+});
+
+test('getKnownColumnConfigKeys ignores missing colnames and unnamed datasource 
fields', () => {
+  expect(getKnownColumnConfigKeys()).toEqual([]);
+  expect(getKnownColumnConfigKeys(undefined, null)).toEqual([]);
+  expect(
+    getKnownColumnConfigKeys(['kept'], {
+      columns: null,
+      metrics: [{}, { metric_name: '' }],
+    }),
+  ).toEqual(['kept']);
+  expect(
+    getKnownColumnConfigKeys(['kept'], {
+      columns: [{}, { column_name: 'region' }],
+      metrics: null,
+    }),
+  ).toEqual(['kept', 'region']);
+});
+
+test('normalizeColumnConfigKeys remaps display aliases onto real column keys', 
() => {
+  expect(
+    normalizeColumnConfigKeys(
+      {
+        Time: { customColumnName: 'Date' },
+        region: { columnWidth: 50 },
+      },
+      ['__timestamp', 'region'],
+    ),
+  ).toEqual({
+    __timestamp: { customColumnName: 'Date' },
+    region: { columnWidth: 50 },
+  });
+});
+
+test('normalizeColumnConfigKeys prefers an existing stored key over a display 
alias', () => {
+  const expected = {
+    __timestamp: { customColumnName: 'Stored' },
+  };
+  expect(
+    normalizeColumnConfigKeys(
+      {
+        __timestamp: { customColumnName: 'Stored' },
+        Time: { customColumnName: 'Alias' },
+      },
+      ['__timestamp'],
+    ),
+  ).toEqual(expected);
+  expect(
+    normalizeColumnConfigKeys(
+      {
+        Time: { customColumnName: 'Alias' },
+        __timestamp: { customColumnName: 'Stored' },
+      },
+      ['__timestamp'],
+    ),
+  ).toEqual(expected);
+});
+
+test('normalizeColumnConfigKeys keeps a real Time column when it is not in 
colnames but __timestamp is', () => {
+  expect(
+    normalizeColumnConfigKeys(
+      { Time: { customColumnName: 'Keep me' } },
+      ['__timestamp'],
+      ['Time', '__timestamp'],
+    ),
+  ).toEqual({
+    Time: { customColumnName: 'Keep me' },
+  });
+});
+
+test('normalizeColumnConfigKeys keeps a current Time column when __timestamp 
is also present', () => {
+  expect(
+    normalizeColumnConfigKeys({ Time: { customColumnName: 'Keep me' } }, [
+      'Time',
+      '__timestamp',
+    ]),
+  ).toEqual({
+    Time: { customColumnName: 'Keep me' },
+  });
+});
+
+test('normalizeColumnConfigKeys remaps English and translated Time aliases', 
() => {
+  const originalAlias = COLUMN_NAME_ALIASES[DTTM_ALIAS];
+  COLUMN_NAME_ALIASES[DTTM_ALIAS] = 'Tiempo';
+
+  try {
+    expect(
+      normalizeColumnConfigKeys({ Tiempo: { customColumnName: 'Fecha' } }, [
+        '__timestamp',
+      ]),
+    ).toEqual({
+      __timestamp: { customColumnName: 'Fecha' },
+    });
+    expect(
+      normalizeColumnConfigKeys({ Time: { customColumnName: 'Date' } }, [
+        '__timestamp',
+      ]),
+    ).toEqual({
+      __timestamp: { customColumnName: 'Date' },
+    });
+  } finally {
+    COLUMN_NAME_ALIASES[DTTM_ALIAS] = originalAlias;
+  }
+});
+
+test('normalizeColumnConfigKeys skips remapping when a display alias is 
ambiguous', () => {
+  const originalAliases = { ...COLUMN_NAME_ALIASES };
+  Object.keys(COLUMN_NAME_ALIASES).forEach(key => {
+    delete COLUMN_NAME_ALIASES[key];
+  });
+  Object.assign(COLUMN_NAME_ALIASES, {
+    __timestamp: 'Time',
+    other_time: 'Time',
+  });
+
+  try {
+    expect(
+      normalizeColumnConfigKeys({ Time: { customColumnName: 'Date' } }, [
+        '__timestamp',
+        'other_time',
+      ]),
+    ).toEqual({
+      Time: { customColumnName: 'Date' },
+    });
+  } finally {
+    Object.keys(COLUMN_NAME_ALIASES).forEach(key => {
+      delete COLUMN_NAME_ALIASES[key];
+    });
+    Object.assign(COLUMN_NAME_ALIASES, originalAliases);
+  }
+});
+
 test('buildHeaderGroupRows fills empty cells for ungrouped columns', () => {
   const rows = buildHeaderGroupRows(chartGroups, ['revenue', 'region']);
 
diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/headerGroups.ts
 
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/headerGroups.ts
index aea45f87024..39fab758392 100644
--- 
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/headerGroups.ts
+++ 
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/headerGroups.ts
@@ -20,6 +20,7 @@
 import { t } from '@apache-superset/core/translation';
 import {
   ComparisonType,
+  DTTM_ALIAS,
   ensureIsArray,
   getColumnLabel,
   getMetricLabel,
@@ -29,6 +30,7 @@ import {
   SqlaFormData,
 } from '@superset-ui/core';
 import { isEmpty, last } from 'lodash-es';
+import { COLUMN_NAME_ALIASES } from '../constants';
 import {
   isPercentMetric,
   isRegularMetric,
@@ -61,6 +63,34 @@ export type HeaderGroupCell = {
 
 export const TIME_COMPARE_MAIN_KEY = 'Main';
 
+/** English msgid for `__timestamp`; `COLUMN_NAME_ALIASES` holds `t('Time')`. 
*/
+const DTTM_ALIAS_SOURCE_LABEL = 'Time';
+
+function getColumnNameAliasLabels(col: string): string[] {
+  const translated = COLUMN_NAME_ALIASES[col];
+  const source = col === DTTM_ALIAS ? DTTM_ALIAS_SOURCE_LABEL : undefined;
+  return [
+    ...new Set(
+      [source, translated].filter((label): label is string => Boolean(label)),
+    ),
+  ];
+}
+
+/** Colnames plus dataset columns/metrics, so a real `Time` name is not 
remapped onto `__timestamp`. */
+export function getKnownColumnConfigKeys(
+  colnames: string[] = [],
+  datasource?: {
+    columns?: { column_name?: string }[] | null;
+    metrics?: { metric_name?: string }[] | null;
+  } | null,
+): string[] {
+  return [
+    ...colnames,
+    ...(datasource?.columns ?? []).map(col => col.column_name ?? ''),
+    ...(datasource?.metrics ?? []).map(metric => metric.metric_name ?? ''),
+  ].filter(Boolean);
+}
+
 const TIME_COMPARE_SYMBOL_PREFIXES = ['#', '△', '%'] as const;
 
 type TimeCompareSlot = {
@@ -329,22 +359,50 @@ export function syncTimeComparisonGroups(
  * Remap saved comparison-column config onto locale-independent keys.
  *
  * Charts created before comparison slots were stored as `Main …` may still
- * keep `t('Main') …` entries (for example `Principale revenue`). The column
- * editor lists the stored keys, so those legacy entries must be rewritten
- * before it filters `column_config` down to `colnames`.
+ * keep `t('Main') …` entries (for example `Principale revenue`). A buggy
+ * column editor also wrote `COLUMN_NAME_ALIASES` display names (for example
+ * `Time` for `__timestamp`). The column editor lists the stored keys, so
+ * those legacy entries must be rewritten before it filters `column_config`
+ * down to `colnames`.
  */
 export function normalizeColumnConfigKeys<T>(
   value: Record<string, T> | null | undefined,
   colnames: string[],
+  knownKeys: string[] = colnames,
 ): Record<string, T> {
   if (!value) {
     return {};
   }
   const colnamesSet = new Set(colnames);
-  const next: Record<string, T> = {};
+  const knownKeySet = new Set(knownKeys);
+  const aliasToKey = new Map<string, string>();
+  const ambiguousAliases = new Set<string>();
+  colnames.forEach(col => {
+    getColumnNameAliasLabels(col).forEach(alias => {
+      // A saved key that names a real column or metric is not a legacy alias.
+      if (!alias || knownKeySet.has(alias) || ambiguousAliases.has(alias)) {
+        return;
+      }
+      const existing = aliasToKey.get(alias);
+      if (existing && existing !== col) {
+        aliasToKey.delete(alias);
+        ambiguousAliases.add(alias);
+        return;
+      }
+      aliasToKey.set(alias, col);
+    });
+  });
+  const next: Record<string, T> = Object.create(null);
   Object.entries(value).forEach(([key, config]) => {
+    if (colnamesSet.has(key) || knownKeySet.has(key)) {
+      next[key] = config;
+      return;
+    }
     const stored = toStoredTimeComparisonColumnKey(key, colnames);
-    const target = colnamesSet.has(stored) ? stored : key;
+    let target = colnamesSet.has(stored) ? stored : key;
+    if (!colnamesSet.has(target) && aliasToKey.has(key)) {
+      target = aliasToKey.get(key) as string;
+    }
     if (key === target || !(target in next)) {
       next[target] = config;
     }
diff --git 
a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/controlPanel.tsx 
b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/controlPanel.tsx
index af919d8b03a..13ef3b9d749 100644
--- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/controlPanel.tsx
@@ -46,6 +46,7 @@ import {
   ColorSchemeEnum,
   getHeaderGroupsControlProps,
   getTimeComparisonColumnKeys,
+  getKnownColumnConfigKeys,
 } from '@superset-ui/chart-controls';
 import { t } from '@apache-superset/core/translation';
 import {
@@ -779,6 +780,10 @@ const config: ControlPanelConfig = {
                     coltypes,
                     childColumnMap,
                     timeComparisonColumnMap,
+                    knownKeys: getKnownColumnConfigKeys(
+                      _colnames || [],
+                      explore.datasource,
+                    ),
                   },
                 };
               },
diff --git 
a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts 
b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts
index 1d168c4579c..5c142651d1e 100644
--- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts
@@ -45,6 +45,8 @@ import {
   ConditionalFormattingConfig,
   getColorFormatters,
   ColorSchemeEnum,
+  normalizeColumnConfigKeys,
+  getKnownColumnConfigKeys,
   resolveHeaderGroups,
   toTotalsAggregate,
 } from '@superset-ui/chart-controls';
@@ -428,6 +430,11 @@ const processColumns = memoizePerChart(function 
processColumns(
   const metricsSet = new Set(metrics);
   const percentMetricsSet = new Set(percentMetrics);
   const rawPercentMetricsSet = new Set(rawPercentMetrics);
+  const normalizedColumnConfig = normalizeColumnConfigKeys(
+    columnConfig,
+    colnames || [],
+    getKnownColumnConfigKeys(colnames || [], props.datasource),
+  );
 
   const columns: DataColumnMeta[] = (colnames || [])
     .map((key: string, originalIndex: number) => ({ key, originalIndex }))
@@ -442,7 +449,7 @@ const processColumns = memoizePerChart(function 
processColumns(
       // an earlier column (e.g. a percent-metric-only one) got filtered
       // out, which would otherwise shift every later column's dataType.
       const dataType = coltypes[originalIndex];
-      const config = columnConfig[key] || {};
+      const config = normalizedColumnConfig[key] || {};
       // for the purpose of presentation, only numeric values are treated as 
metrics
       // because users can also add things like `MAX(str_col)` as a metric.
       const isMetric = metricsSet.has(key) && isNumeric(key, records);
diff --git 
a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/isEqualColumns.ts
 
b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/isEqualColumns.ts
index e907ac50ac3..0466e725928 100644
--- 
a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/isEqualColumns.ts
+++ 
b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/isEqualColumns.ts
@@ -69,6 +69,14 @@ export default function isEqualColumns(
     verboseMap: isEqual(a.datasource?.verboseMap, b.datasource?.verboseMap),
     currencyCodeColumn:
       a.datasource?.currencyCodeColumn === b.datasource?.currencyCodeColumn,
+    knownColumnNames: isEqualArray(
+      (a.datasource?.columns ?? []).map(col => col.column_name),
+      (b.datasource?.columns ?? []).map(col => col.column_name),
+    ),
+    knownMetricNames: isEqualArray(
+      (a.datasource?.metrics ?? []).map(metric => metric.metric_name),
+      (b.datasource?.metrics ?? []).map(metric => metric.metric_name),
+    ),
     detectedCurrency:
       a.queriesData?.[0]?.detected_currency ===
       b.queriesData?.[0]?.detected_currency,
diff --git 
a/superset-frontend/plugins/plugin-chart-ag-grid-table/test/controlPanel.test.tsx
 
b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/controlPanel.test.tsx
index b153eb6b6b1..a86303efbcc 100644
--- 
a/superset-frontend/plugins/plugin-chart-ag-grid-table/test/controlPanel.test.tsx
+++ 
b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/controlPanel.test.tsx
@@ -315,10 +315,44 @@ test('column_config mapStateToProps expands comparison 
columns for metrics', ()
   expect(result.columnsPropsObject.colnames).toEqual(
     expect.arrayContaining(['Main col1', '# col1', '△ col1', '% col1']),
   );
+  expect(result.columnsPropsObject.knownKeys).toEqual(
+    expect.arrayContaining(['col1', 'col2']),
+  );
   expect(result.columnsPropsObject.childColumnMap['Main col1']).toBe(false);
   expect(result.columnsPropsObject.childColumnMap['# col1']).toBe(true);
 });
 
+test('column_config knownKeys keep original Time metric after comparison 
expansion', () => {
+  const controlConfig = findNamedControl('column_config');
+  const explore = {
+    ...createMockExplore(['1 year ago']),
+    form_data: {
+      ...createMockExplore(['1 year ago']).form_data,
+      metrics: ['Time'],
+    },
+  };
+  const result = controlConfig!.mapStateToProps!(
+    explore,
+    createMockControlStateForConditionalFormatting(),
+    {
+      chartStatus: 'success' as const,
+      queriesResponse: [
+        {
+          colnames: ['__timestamp', 'Time'],
+          coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
+        },
+      ],
+    },
+  );
+
+  expect(result.columnsPropsObject.colnames).toEqual(
+    expect.arrayContaining(['__timestamp', 'Main Time', '# Time']),
+  );
+  expect(result.columnsPropsObject.knownKeys).toEqual(
+    expect.arrayContaining(['__timestamp', 'Time']),
+  );
+});
+
 test('metrics control includes non-filterable columns', () => {
   const mapStateToProps = findMetricsMapStateToProps();
   expect(mapStateToProps).toBeTruthy();
diff --git 
a/superset-frontend/plugins/plugin-chart-ag-grid-table/test/transformProps.test.ts
 
b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/transformProps.test.ts
index 9d7b75ff04e..0f2107425a6 100644
--- 
a/superset-frontend/plugins/plugin-chart-ag-grid-table/test/transformProps.test.ts
+++ 
b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/transformProps.test.ts
@@ -778,3 +778,162 @@ test('drops time comparison header groups when 
time_compare is empty', () => {
 
   expect(result.headerGroups).toEqual([]);
 });
+
+test('does not remap a real Previous revenue column onto Main revenue', () => {
+  const result = transformProps(
+    createMockChartProps({
+      rawFormData: {
+        viz_type: 'table',
+        datasource: '1__table',
+        query_mode: QueryMode.Raw,
+        metrics: [],
+        percent_metrics: [],
+        column_config: {
+          'Previous revenue': { customColumnName: 'Prior' },
+        },
+        table_timestamp_format: '',
+      },
+      queriesData: [
+        {
+          data: [
+            {
+              'Main revenue': 10,
+              'Previous revenue': 8,
+              '# revenue': 2,
+            },
+          ],
+          colnames: ['Main revenue', 'Previous revenue', '# revenue'],
+          coltypes: [
+            GenericDataType.Numeric,
+            GenericDataType.Numeric,
+            GenericDataType.Numeric,
+          ],
+          rowcount: 1,
+          applied_filters: [],
+          rejected_filters: [],
+        },
+      ] as unknown as TableChartProps['queriesData'],
+    }),
+  );
+
+  expect(
+    result.columns.find(col => col.key === 'Previous revenue')?.config,
+  ).toEqual(expect.objectContaining({ customColumnName: 'Prior' }));
+  expect(
+    result.columns.find(col => col.key === 'Main revenue')?.config,
+  ).toEqual({});
+});
+
+test('does not apply a dropped Previous revenue dataset column config to Main 
revenue', () => {
+  const result = transformProps(
+    createMockChartProps({
+      rawFormData: {
+        viz_type: 'table',
+        datasource: '1__table',
+        query_mode: QueryMode.Aggregate,
+        metrics: [],
+        percent_metrics: [],
+        column_config: {
+          'Previous revenue': { visible: false },
+        },
+        table_timestamp_format: '',
+      },
+      datasource: {
+        columns: [{ column_name: 'Previous revenue' }],
+        metrics: [],
+        columnFormats: {},
+        currencyFormats: {},
+        verboseMap: {},
+      } as unknown as TableChartProps['datasource'],
+      queriesData: [
+        {
+          data: [{ 'Main revenue': 10, '# revenue': 2 }],
+          colnames: ['Main revenue', '# revenue'],
+          coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
+          rowcount: 1,
+          applied_filters: [],
+          rejected_filters: [],
+        },
+      ] as unknown as TableChartProps['queriesData'],
+    }),
+  );
+
+  expect(
+    result.columns.find(col => col.key === 'Main revenue')?.config,
+  ).toEqual({});
+});
+
+test('applies legacy Time column_config to the __timestamp column', () => {
+  const result = transformProps(
+    createMockChartProps({
+      rawFormData: {
+        viz_type: 'table',
+        datasource: '1__table',
+        query_mode: QueryMode.Aggregate,
+        metrics: [],
+        percent_metrics: [],
+        column_config: {
+          Time: { customColumnName: 'Date', d3TimeFormat: '%Y' },
+        },
+        table_timestamp_format: '',
+      },
+      queriesData: [
+        {
+          data: [{ __timestamp: '2020-01-01T12:34:56' }],
+          colnames: ['__timestamp'],
+          coltypes: [GenericDataType.Temporal],
+          rowcount: 1,
+          applied_filters: [],
+          rejected_filters: [],
+        },
+      ] as unknown as TableChartProps['queriesData'],
+    }),
+  );
+
+  const timestampColumn = result.columns.find(col => col.key === 
'__timestamp');
+  expect(timestampColumn?.config).toEqual(
+    expect.objectContaining({
+      customColumnName: 'Date',
+      d3TimeFormat: '%Y',
+    }),
+  );
+});
+
+test('does not apply a dropped Time dataset column config to __timestamp', () 
=> {
+  const result = transformProps(
+    createMockChartProps({
+      rawFormData: {
+        viz_type: 'table',
+        datasource: '1__table',
+        query_mode: QueryMode.Aggregate,
+        metrics: [],
+        percent_metrics: [],
+        column_config: {
+          Time: { customColumnName: 'Date', d3TimeFormat: '%Y' },
+        },
+        table_timestamp_format: '',
+      },
+      datasource: {
+        columns: [{ column_name: 'Time' }],
+        metrics: [],
+        columnFormats: {},
+        currencyFormats: {},
+        verboseMap: {},
+      } as unknown as TableChartProps['datasource'],
+      queriesData: [
+        {
+          data: [{ __timestamp: '2020-01-01T12:34:56' }],
+          colnames: ['__timestamp'],
+          coltypes: [GenericDataType.Temporal],
+          rowcount: 1,
+          applied_filters: [],
+          rejected_filters: [],
+        },
+      ] as unknown as TableChartProps['queriesData'],
+    }),
+  );
+
+  expect(result.columns.find(col => col.key === 
'__timestamp')?.config).toEqual(
+    {},
+  );
+});
diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx 
b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx
index c11d4dab16b..f8e8a56eb5f 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx
@@ -34,6 +34,7 @@ import {
   ControlFormItemSpec,
   getStandardizedControls,
   DEFAULT_TIME_FORMAT,
+  getKnownColumnConfigKeys,
 } from '@superset-ui/chart-controls';
 import { DEFAULT_FORM_DATA } from './types';
 import { LabelPositionEnum } from '../types';
@@ -224,7 +225,14 @@ const config: ControlPanelConfig = {
                     | ChartDataResponseResult
                     | undefined,
                   appliedColumnNames: metricColumn,
-                  columnsPropsObject: { colnames, coltypes },
+                  columnsPropsObject: {
+                    colnames,
+                    coltypes,
+                    knownKeys: getKnownColumnConfigKeys(
+                      colnames,
+                      explore.datasource,
+                    ),
+                  },
                 };
               },
             },
diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx 
b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
index 390ecff1a0c..603798bd9f2 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
@@ -43,6 +43,7 @@ import {
   hasMatchingDatasourceMetadata,
   getHeaderGroupsControlProps,
   getTimeComparisonColumnKeys,
+  getKnownColumnConfigKeys,
   ConditionalFormattingConfig,
   ObjectFormattingEnum,
   ColorSchemeEnum,
@@ -785,6 +786,10 @@ const config: ControlPanelConfig = {
                     coltypes,
                     childColumnMap,
                     timeComparisonColumnMap,
+                    knownKeys: getKnownColumnConfigKeys(
+                      _colnames || [],
+                      explore.datasource,
+                    ),
                   },
                 };
               },
diff --git a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts 
b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
index e3b2d866c80..ac5d52de768 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
@@ -44,6 +44,8 @@ import {
   ConditionalFormattingConfig,
   getColorFormatters,
   ColorSchemeEnum,
+  normalizeColumnConfigKeys,
+  getKnownColumnConfigKeys,
   resolveHeaderGroups,
 } from '@superset-ui/chart-controls';
 
@@ -237,6 +239,11 @@ const processColumns = memoizeOne(function processColumns(
   const columnsByName = new Map(
     (props.datasource.columns ?? []).map(col => [col.column_name, col]),
   );
+  const normalizedColumnConfig = normalizeColumnConfigKeys(
+    columnConfig,
+    colnames || [],
+    getKnownColumnConfigKeys(colnames || [], props.datasource),
+  );
 
   const columns: DataColumnMeta[] = (colnames || [])
     .filter(
@@ -246,7 +253,7 @@ const processColumns = memoizeOne(function processColumns(
     )
     .map((key: string, i) => {
       const dataType = coltypes[i];
-      const config = columnConfig[key] || {};
+      const config = normalizedColumnConfig[key] || {};
       // for the purpose of presentation, only numeric values are treated as 
metrics
       // because users can also add things like `MAX(str_col)` as a metric.
       const isFilterable = columnsByName.get(key)?.filterable;
diff --git 
a/superset-frontend/plugins/plugin-chart-table/src/utils/isEqualColumns.ts 
b/superset-frontend/plugins/plugin-chart-table/src/utils/isEqualColumns.ts
index 0643eaab68b..2c8699624da 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/utils/isEqualColumns.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/utils/isEqualColumns.ts
@@ -30,6 +30,14 @@ export default function isEqualColumns(
     a.datasource.currencyFormats === b.datasource.currencyFormats &&
     a.datasource.currencyCodeColumn === b.datasource.currencyCodeColumn &&
     a.datasource.verboseMap === b.datasource.verboseMap &&
+    isEqualArray(
+      (a.datasource.columns ?? []).map(col => col.column_name),
+      (b.datasource.columns ?? []).map(col => col.column_name),
+    ) &&
+    isEqualArray(
+      (a.datasource.metrics ?? []).map(metric => metric.metric_name),
+      (b.datasource.metrics ?? []).map(metric => metric.metric_name),
+    ) &&
     a.formData.tableTimestampFormat === b.formData.tableTimestampFormat &&
     a.formData.timeGrainSqla === b.formData.timeGrainSqla &&
     JSON.stringify(a.formData.columnConfig || null) ===
diff --git 
a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx 
b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
index 4192e9098cb..620c49134d6 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
@@ -390,6 +390,118 @@ describe('plugin-chart-table', () => {
       expect(String(parsedDate)).toBe('2020-01-01 12:34:56');
       expect(parsedDate.getTime()).toBe(1577882096000);
     });
+
+    test('does not remap a real Previous revenue column onto Main revenue', () 
=> {
+      const { columns } = transformProps({
+        ...testData.basic,
+        queriesData: [
+          {
+            ...testData.basic.queriesData[0],
+            colnames: ['Main revenue', 'Previous revenue', '# revenue'],
+            coltypes: [
+              GenericDataType.Numeric,
+              GenericDataType.Numeric,
+              GenericDataType.Numeric,
+            ],
+            data: [
+              {
+                'Main revenue': 10,
+                'Previous revenue': 8,
+                '# revenue': 2,
+              },
+            ],
+          },
+        ],
+        rawFormData: {
+          ...testData.basic.rawFormData,
+          column_config: {
+            'Previous revenue': { customColumnName: 'Prior' },
+          },
+        },
+      });
+
+      expect(
+        columns.find(col => col.key === 'Previous revenue')?.config,
+      ).toEqual(expect.objectContaining({ customColumnName: 'Prior' }));
+      expect(columns.find(col => col.key === 'Main revenue')?.config).toEqual(
+        {},
+      );
+    });
+
+    test('does not apply a dropped Previous revenue dataset column config to 
Main revenue', () => {
+      const { columns } = transformProps({
+        ...testData.basic,
+        datasource: {
+          ...testData.basic.datasource,
+          columns: [{ column_name: 'Previous revenue' }],
+        },
+        queriesData: [
+          {
+            ...testData.basic.queriesData[0],
+            colnames: ['Main revenue', '# revenue'],
+            coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
+            data: [{ 'Main revenue': 10, '# revenue': 2 }],
+          },
+        ],
+        rawFormData: {
+          ...testData.basic.rawFormData,
+          column_config: {
+            'Previous revenue': { visible: false },
+          },
+        },
+      });
+
+      expect(columns.find(col => col.key === 'Main revenue')?.config).toEqual(
+        {},
+      );
+    });
+
+    test('applies legacy Time column_config to the __timestamp column', () => {
+      const { columns } = transformProps({
+        ...testData.basic,
+        rawFormData: {
+          ...testData.basic.rawFormData,
+          column_config: {
+            Time: { customColumnName: 'Date', d3TimeFormat: '%Y' },
+          },
+        },
+      });
+      const timestampColumn = columns.find(col => col.key === '__timestamp');
+
+      expect(timestampColumn?.config).toEqual(
+        expect.objectContaining({
+          customColumnName: 'Date',
+          d3TimeFormat: '%Y',
+        }),
+      );
+    });
+
+    test('does not apply a dropped Time dataset column config to __timestamp', 
() => {
+      const { columns } = transformProps({
+        ...testData.basic,
+        datasource: {
+          ...testData.basic.datasource,
+          columns: [
+            {
+              column_name: 'Time',
+              type: 'TIMESTAMP',
+              type_generic: GenericDataType.Temporal,
+            },
+          ],
+        },
+        rawFormData: {
+          ...testData.basic.rawFormData,
+          column_config: {
+            Time: { customColumnName: 'Date', d3TimeFormat: '%Y' },
+          },
+        },
+      });
+
+      expect(columns.find(col => col.key === '__timestamp')?.config).toEqual(
+        {},
+      );
+    });
+
     test('should process comparison columns when time_compare and 
comparison_type are set', () => {
       const transformedProps = transformProps(testData.comparison);
       const comparisonColumns = transformedProps.columns.filter(
diff --git 
a/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx 
b/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx
index 30cee93b656..6fb6ba355cb 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx
@@ -906,10 +906,44 @@ test('column_config mapStateToProps expands comparison 
columns for metrics', ()
   expect(result.columnsPropsObject.colnames).toEqual(
     expect.arrayContaining(['Main col1', '# col1', '△ col1', '% col1']),
   );
+  expect(result.columnsPropsObject.knownKeys).toEqual(
+    expect.arrayContaining(['col1', 'col2']),
+  );
   expect(result.columnsPropsObject.childColumnMap['Main col1']).toBe(false);
   expect(result.columnsPropsObject.childColumnMap['# col1']).toBe(true);
 });
 
+test('column_config knownKeys keep original Time metric after comparison 
expansion', () => {
+  const controlConfig = findNamedControl('column_config');
+  const explore = {
+    ...createMockExplore(['1 year ago']),
+    form_data: {
+      ...createMockExplore(['1 year ago']).form_data,
+      metrics: ['Time'],
+    },
+  };
+  const result = controlConfig!.mapStateToProps!(
+    explore,
+    createMockControlStateForConditionalFormatting(),
+    {
+      chartStatus: 'success' as const,
+      queriesResponse: [
+        {
+          colnames: ['__timestamp', 'Time'],
+          coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
+        },
+      ],
+    },
+  );
+
+  expect(result.columnsPropsObject.colnames).toEqual(
+    expect.arrayContaining(['__timestamp', 'Main Time', '# Time']),
+  );
+  expect(result.columnsPropsObject.knownKeys).toEqual(
+    expect.arrayContaining(['__timestamp', 'Time']),
+  );
+});
+
 test.each([false, true])(
   'numeric-leading provider uid retains pagination capability and reset 
advice: %s',
   supportsOffset => {
diff --git 
a/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.test.tsx
 
b/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.test.tsx
index 2637e1134a0..08f83a349bd 100644
--- 
a/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.test.tsx
+++ 
b/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.test.tsx
@@ -84,3 +84,78 @@ test('keeps remapped Main column settings when another 
column is edited', async
     region: { columnWidth: 50 },
   });
 });
+
+test('stores config for an aliased column under its real column key', async () 
=> {
+  const onChange = jest.fn();
+  render(
+    <ColumnConfigControl
+      name="column_config"
+      value={{
+        __timestamp: { customColumnName: 'Date' },
+      }}
+      onChange={onChange}
+      columnsPropsObject={{
+        colnames: ['__timestamp'],
+        coltypes: [GenericDataType.Temporal],
+      }}
+    />,
+  );
+
+  await userEvent.click(screen.getByRole('button', { name: 'Configure Time' 
}));
+
+  expect(onChange).toHaveBeenCalledWith({
+    __timestamp: { customColumnName: 'Date', columnWidth: 50 },
+  });
+});
+
+test('migrates legacy Time alias config onto __timestamp when edited', async 
() => {
+  const onChange = jest.fn();
+  render(
+    <ColumnConfigControl
+      name="column_config"
+      value={{
+        Time: { customColumnName: 'Date' },
+      }}
+      onChange={onChange}
+      columnsPropsObject={{
+        colnames: ['__timestamp', 'region'],
+        coltypes: [GenericDataType.Temporal, GenericDataType.String],
+      }}
+    />,
+  );
+
+  await userEvent.click(
+    screen.getByRole('button', { name: 'Configure region' }),
+  );
+
+  expect(onChange).toHaveBeenCalledWith({
+    __timestamp: { customColumnName: 'Date' },
+    region: { columnWidth: 50 },
+  });
+});
+
+test('does not store a known Time column on __timestamp when another column is 
edited', async () => {
+  const onChange = jest.fn();
+  render(
+    <ColumnConfigControl
+      name="column_config"
+      value={{
+        Time: { customColumnName: 'Date' },
+      }}
+      onChange={onChange}
+      columnsPropsObject={{
+        colnames: ['__timestamp', 'region'],
+        coltypes: [GenericDataType.Temporal, GenericDataType.String],
+        knownKeys: ['Time', '__timestamp', 'region'],
+      }}
+    />,
+  );
+
+  await userEvent.click(
+    screen.getByRole('button', { name: 'Configure region' }),
+  );
+
+  expect(onChange).toHaveBeenCalledWith({
+    region: { columnWidth: 50 },
+  });
+});
diff --git 
a/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx
 
b/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx
index 2481fc0bbfd..6647e95641b 100644
--- 
a/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx
+++ 
b/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx
@@ -42,6 +42,7 @@ export type ColumnConfigControlProps<T extends ColumnConfig> =
       coltypes: GenericDataType[];
       childColumnMap?: Record<string, boolean>;
       timeComparisonColumnMap?: Record<string, boolean>;
+      knownKeys?: string[];
     };
     configFormLayout?: ColumnConfigFormLayout;
     appliedColumnNames?: string[];
@@ -67,7 +68,11 @@ export default function ColumnConfigControl<T extends 
ColumnConfig>({
   height,
   ...props
 }: ColumnConfigControlProps<T>) {
-  const { colnames: _colnames, coltypes: _coltypes } = columnsPropsObject || 
{};
+  const {
+    colnames: _colnames,
+    coltypes: _coltypes,
+    knownKeys,
+  } = columnsPropsObject || {};
   let colnames: string[] = [];
   let coltypes: GenericDataType[] = [];
   if (appliedColumnNames.length === 0) {
@@ -83,15 +88,20 @@ export default function ColumnConfigControl<T extends 
ColumnConfig>({
     });
   }
   const theme = useTheme();
+  const columnConfigKnownKeys = useMemo(
+    () => [...new Set([...(knownKeys ?? []), ...colnames])],
+    [colnames, knownKeys],
+  );
   const normalizedValue = useMemo(
-    () => normalizeColumnConfigKeys(value, colnames),
-    [colnames, value],
+    () => normalizeColumnConfigKeys(value, colnames, columnConfigKnownKeys),
+    [colnames, columnConfigKnownKeys, value],
   );
 
   const columnConfigs = useMemo(() => {
     const configs: Record<string, ColumnConfigInfo> = {};
     colnames?.forEach((col, idx) => {
       configs[col] = {
+        key: col,
         name: COLUMN_NAME_ALIASES[col] || col,
         type: coltypes?.[idx],
         config: normalizedValue[col] || {},
@@ -145,9 +155,9 @@ export default function ColumnConfigControl<T extends 
ColumnConfig>({
       >
         {columnsWithChildInfo.map(col => (
           <ColumnConfigItem
-            key={col.name}
+            key={col.key}
             column={col}
-            onChange={config => setColumnConfig(col.name, config as T)}
+            onChange={config => setColumnConfig(col.key, config as T)}
             configFormLayout={
               col.isTimeComparisonColumn
                 ? ({
diff --git 
a/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts
 
b/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts
index a689c754fbd..6461617452f 100644
--- 
a/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts
+++ 
b/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts
@@ -41,6 +41,7 @@ export type ColumnConfig = {
 export interface ColumnConfigInfo {
   isChildColumn: boolean;
   isTimeComparisonColumn: boolean;
+  key: string;
   name: string;
   type?: GenericDataType;
   config: JsonObject;

Reply via email to