[Bug] Table chart applies Time grain to only the first temporal column (regression from 4.0.2)

Open Beginner friendly
#45,964 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 2 days

@PrakharAgarwal17 is already working on this.

Since Nov 4, 2026.

  • #46008 by @PrakharAgarwal17 — open

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
72/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
react, sql, typescript

Research direction

The bug is in superset-frontend/plugins/plugin-chart-table/src/buildQuery.ts, where a temporalColumnAdded flag lets only the first temporal column receive the time grain; the same pattern exists in plugins/plugin-chart-ag-grid-table/src/buildQuery.ts. Start by reading that code path and reproducing the generated SQL with two temporal dimensions and Time grain set. Done when every physical temporal column used as a dimension gets the grain expression and the existing table-chart buildQuery tests still pass.

Written by the indexing model from the issue text.

Description

#bug 🦾 ai-candidate
Bug description

When a Table chart has two or more temporal (date) dimensions, the "Time grain" setting is applied to only the first temporal column. The remaining temporal columns are sent to the database as raw columns, so rows that differ only by day/time no longer collapse into one row. This is a regression: the same chart works correctly on 4.0.2.

Example: with Dimensions StartDate, EndDate and Time grain Month, the chart returns three rows that all display as "Aug 2026 / Sep 2026" but have different counts (4, 1, 1), because EndDate is not grouped by month.

Steps to reproduce
  1. Use a dataset (or physical table) that has two temporal columns, e.g. StartDate and EndDate.
  2. Create a Table chart (table_v2).
  3. Set Query mode = Aggregate.
  4. Dimensions: StartDate, EndDate.
  5. Metric: COUNT(DISTINCT Counts) (any aggregate metric).
  6. Set Time grain = Month.
  7. Click "Update chart" and inspect the generated SQL ("View query").

Minimal form data:

{
  "viz_type": "table",
  "query_mode": "aggregate",
  "granularity_sqla": "StartDate",
  "time_grain_sqla": "P1M",
  "columns": ["StartDate", "EndDate"],
  "metrics": ["COUNT_DISTINCT(Counts)"],
  "temporal_columns_lookup": { "StartDate": true, "EndDate": true }
}
Expected behavior

Time grain should be applied to every temporal column that is used as a dimension (the 4.0.2 behavior), i.e. both StartDate and EndDate should be truncated to month and grouped. Expected SQL:

SELECT
  TO_DATE(DATE_SUB(`StartDate`, INTERVAL (DAYOFMONTH(`StartDate`) - 1) DAY)) AS `StartDate`,
  TO_DATE(DATE_SUB(`EndDate`,   INTERVAL (DAYOFMONTH(`EndDate`)   - 1) DAY)) AS `EndDate`,
  COUNT(DISTINCT `Counts`) AS `Counts`
FROM `activity`
WHERE ...
GROUP BY
  TO_DATE(DATE_SUB(`StartDate`, INTERVAL (DAYOFMONTH(`StartDate`) - 1) DAY)),
  TO_DATE(DATE_SUB(`EndDate`,   INTERVAL (DAYOFMONTH(`EndDate`)   - 1) DAY))

Result: one row — Aug 2026 | Sep 2026 | 6.

Actual behavior

Only the first temporal column is converted to a time-grain expression; the second one is passed through unchanged:

SELECT
  TO_DATE(DATE_SUB(`StartDate`, INTERVAL (DAYOFMONTH(`StartDate`) - 1) DAY)) AS `StartDate`,
  `EndDate` AS `EndDate`,
  COUNT(DISTINCT `Counts`) AS `Counts`
FROM `activity`
WHERE
  `StartDate` >= STR_TO_DATE('2026-08-01', '%Y-%m-%d')
  AND `StartDate` < STR_TO_DATE('2026-08-31', '%Y-%m-%d')
  AND `EndDate` >= STR_TO_DATE('2026-09-01', '%Y-%m-%d')
  AND `EndDate` < STR_TO_DATE('2026-09-30', '%Y-%m-%d')
GROUP BY
  TO_DATE(DATE_SUB(`StartDate`, INTERVAL (DAYOFMONTH(`StartDate`) - 1) DAY)),
  `EndDate`
ORDER BY `Counts` DESC
LIMIT 10000

Result (duplicated dimension combinations, all displayed as the same month):

StartDate EndDate Counts
Aug 2026 Sep 2026 4
Aug 2026 Sep 2026 1
Aug 2026 Sep 2026 1

Because the raw EndDate values differ (different days) while the column is rendered as "Sep 2026", the rows look identical to the user but are not aggregated — the numbers silently look wrong.

Root cause

superset-frontend/plugins/plugin-chart-table/src/buildQuery.ts

4.0.2 maps over all columns and replaces each temporal column with an ad-hoc time-grain column (position preserved):

columns = columns.map(col => {
  if (
    isPhysicalColumn(col) &&
    time_grain_sqla &&
    formData?.temporal_columns_lookup?.[col]
  ) {
    return {
      timeGrain: time_grain_sqla,
      columnType: 'BASE_AXIS',
      sqlExpression: col,
      label: col,
      expressionType: 'SQL',
    } as AdhocColumn;
  }
  return col;
});

6.1.0 uses a temporalColumnAdded flag, so only the first matching column is converted; all subsequent temporal columns fall through unchanged. It also removes that column and re-adds it at index 0:

let temporalColumnAdded = false;
let temporalColumn = null;

columns = columns.filter(col => {
  const shouldBeAdded =
    isPhysicalColumn(col) &&
    time_grain_sqla &&
    temporalColumnsLookup?.[col];

  if (shouldBeAdded && !temporalColumnAdded) {
    temporalColumn = {
      timeGrain: time_grain_sqla,
      columnType: 'BASE_AXIS',
      sqlExpression: col,
      label: col,
      expressionType: 'SQL',
    } as AdhocColumn;
    temporalColumnAdded = true;
    return false;
  }
  return true;
});

if (temporalColumn) {
  columns = [temporalColumn, ...columns];
}

The same logic exists in plugins/plugin-chart-ag-grid-table/src/buildQuery.ts (experimental AG Grid Table), so that chart is affected too. Pivot Table does not contain this code path.

Suggested fix

Keep the "temporal column first" presentation behavior if it is intentional, but do not use a single temporalColumnAdded flag to decide whether to apply the grain — apply the grain to every physical temporal column, e.g.:

columns = columns.map(col =>
  isPhysicalColumn(col) && time_grain_sqla && temporalColumnsLookup?.[col]
    ? {
        timeGrain: time_grain_sqla,
        columnType: 'BASE_AXIS',
        sqlExpression: col,
        label: col,
        expressionType: 'SQL',
      }
    : col,
);

and reorder afterwards only if the "first temporal column first" behavior is still desired.

Workaround
  • Use only one temporal dimension per chart, or
  • Wrap the second date dimension as an ad-hoc "Custom SQL" column that truncates explicitly, e.g. DATE_FORMAT(\EndDate`, '%Y-%m-01')` (database-specific), or
  • Pre-truncate the date in the dataset / SQL before charting.

Note: swapping the order of the two dimensions only moves the problem to the other column.

Screenshots/recordings

No response

Superset version

6.1.0

Python version

3.11

Node version

16

Browser

Chrome

Additional context

No response

Checklist
  • I have searched Superset docs and Slack and didn't find a solution to my problem.
  • I have searched the GitHub issue tracker and didn't find a similar bug report.
  • I have checked Superset's logs for errors and if I found a relevant Python stacktrace, I included it here as text in the "additional context" section.
Dominant language
Python
Stars
75.4k
Forks
18.6k
Avg merge
2d 14h
Merged PRs (30d)
603

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from apache/superset

All issues in apache/superset

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.