You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by jl...@apache.org on 2026/09/17 17:05:46 UTC

(superset) branch master updated: fix(time-format): stop reading short digit-only strings as epoch offsets (#44352)

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

sadpandajoe 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 66e64cc7d89 fix(time-format): stop reading short digit-only strings as epoch offsets (#44352)
66e64cc7d89 is described below

commit 66e64cc7d895800be1a5bbee4380650c85eebce7
Author: Joe Li <jo...@preset.io>
AuthorDate: Thu Sep 17 10:05:38 2026 -0700

    fix(time-format): stop reading short digit-only strings as epoch offsets (#44352)
    
    Co-authored-by: sadpandajoe <sa...@users.noreply.github.com>
    Co-authored-by: Claude Sonnet 5 <no...@anthropic.com>
---
 .../src/time-format/utils/stringifyTimeInput.ts    | 21 ++++++++++++----
 .../time-format/utils/stringifyTimeInput.test.ts   | 28 ++++++++++++++++++++++
 .../src/react-pivottable/TableRenderers.tsx        | 15 ++++++------
 .../test/react-pivottable/tableRenders.test.tsx    | 21 ++++++++++++++++
 4 files changed, 72 insertions(+), 13 deletions(-)

diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/utils/stringifyTimeInput.ts b/superset-frontend/packages/superset-ui-core/src/time-format/utils/stringifyTimeInput.ts
index 1573d193ab6..2e1369af711 100644
--- a/superset-frontend/packages/superset-ui-core/src/time-format/utils/stringifyTimeInput.ts
+++ b/superset-frontend/packages/superset-ui-core/src/time-format/utils/stringifyTimeInput.ts
@@ -29,15 +29,26 @@ export default function stringifyTimeInput(
   if (typeof value === 'string') {
     const trimmed = value.trim();
     // A bare four-digit string is the ISO 8601 year-only form ("2017"), which
-    // every engine parses as January 1st of that year. Any other integer
-    // string is an epoch timestamp in milliseconds that was stringified on
-    // its way here, e.g. by the pivot table, and is not a valid Date input.
+    // every engine parses as January 1st of that year. A digit-only string
+    // of at least 10 digits is long enough to plausibly be an epoch timestamp
+    // in milliseconds that was stringified on its way here, e.g. by the pivot
+    // table. A digit-only string that is neither - a YYYYMMDD date key such as
+    // "20260903", a YYYYMM one such as "202609", or a small integer that is
+    // not a timestamp at all - is returned as it came in. Handing those to
+    // `new Date` would read them through an engine-specific legacy parser
+    // that turns "202609" into the year 202609 and "5" into May of 2001,
+    // rather than leaving them alone.
     const isYear = /^\d{4}$/.test(trimmed);
-    const isIntegerString = /^-?\d+$/.test(trimmed);
+    const isPlausibleEpoch = /^-?\d{10,}$/.test(trimmed);
+    const isDigitsOnly = /^-?\d+$/.test(trimmed);
     if (isYear) {
       time = new Date(trimmed);
+    } else if (isPlausibleEpoch) {
+      time = new Date(Number(trimmed));
+    } else if (isDigitsOnly) {
+      return value;
     } else {
-      time = new Date(isIntegerString ? Number(trimmed) : value);
+      time = new Date(value);
     }
   } else {
     time = value instanceof Date ? value : new Date(value);
diff --git a/superset-frontend/packages/superset-ui-core/test/time-format/utils/stringifyTimeInput.test.ts b/superset-frontend/packages/superset-ui-core/test/time-format/utils/stringifyTimeInput.test.ts
index 9fd9b8d68ce..90ef8b86ee5 100644
--- a/superset-frontend/packages/superset-ui-core/test/time-format/utils/stringifyTimeInput.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/time-format/utils/stringifyTimeInput.test.ts
@@ -69,6 +69,34 @@ test('treats a four-digit integer string as a year, not as milliseconds', () =>
   );
 });
 
+test('does not treat a short non-year digit-only string as epoch milliseconds', () => {
+  // A YYYYMMDD date key or bare year-adjacent string like these is neither
+  // the four-digit year form nor long enough to plausibly be an epoch
+  // timestamp, so it must be left unchanged rather than silently rendered
+  // as a moment near 1970.
+  expect(stringifyTimeInput('20260903', format)).toBe('20260903');
+  expect(stringifyTimeInput('19870214', format)).toBe('19870214');
+  expect(stringifyTimeInput('999999999', format)).toBe('999999999');
+});
+
+test('does not let the engine legacy parser guess at a short digit-only string', () => {
+  // `new Date` resolves these through an implementation-specific parser that
+  // reads "202609" as the year 202609 and "5" as May of 2001, so they cannot
+  // be left to the Invalid Date fallback and must be returned as they came in.
+  expect(stringifyTimeInput('202609', format)).toBe('202609');
+  expect(stringifyTimeInput('5', format)).toBe('5');
+  expect(stringifyTimeInput('99', format)).toBe('99');
+  expect(stringifyTimeInput('123', format)).toBe('123');
+  expect(stringifyTimeInput('0', format)).toBe('0');
+  expect(stringifyTimeInput('-1', format)).toBe('-1');
+});
+
+test('treats a ten-digit integer string as epoch milliseconds', () => {
+  expect(stringifyTimeInput('1000000000', format)).toBe(
+    '1970-01-12T13:46:40.000Z',
+  );
+});
+
 test('returns the original input of an unparseable DateWithFormatter without re-entering the formatter', () => {
   // The `${value}` fallback calls `DateWithFormatter.toString()`, which must
   // return the input rather than call the formatter again, or the two would
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx
index 4b28b2a866f..057159bf8ed 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx
@@ -48,16 +48,15 @@ import { Styles } from './Styles';
  * Pivot keys are stringified on their way through `PivotData`, so a temporal
  * header holding an epoch timestamp arrives as e.g. "1700000000000". Coerce
  * such numeric strings back to numbers so temporal formatters (which expect
- * an epoch) render correctly. A bare four-digit string is the ISO 8601
- * year-only form ("2017"), which the shared `stringifyTimeInput` in core
- * reads as that calendar year; coercing it would turn the year into two
- * seconds past 1970, so it is passed through untouched.
+ * an epoch) render correctly. Only a digit-only string of at least 10 digits
+ * is long enough to plausibly be an epoch in milliseconds; shorter ones are
+ * date keys or plain integers - "2017" is the ISO 8601 year-only form and
+ * "20260903" a YYYYMMDD key - and coercing either would render it as a moment
+ * near 1970. Those are passed through as strings, which the shared
+ * `stringifyTimeInput` in core resolves or returns untouched.
  */
 const toDateFormatterInput = (value: unknown): unknown =>
-  typeof value === 'string' &&
-  value.trim() !== '' &&
-  !/^\d{4}$/.test(value.trim()) &&
-  Number.isFinite(Number(value))
+  typeof value === 'string' && /^-?\d{10,}$/.test(value.trim())
     ? Number(value)
     : value;
 
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/tableRenders.test.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/tableRenders.test.tsx
index dca6c3928ff..511d55d5d81 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/tableRenders.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/tableRenders.test.tsx
@@ -728,6 +728,27 @@ test('TableRenderer passes four-digit year strings through to row header date fo
   expect(screen.queryByText('1970')).not.toBeInTheDocument();
 });
 
+test('TableRenderer passes short date keys through to header date formatters uncoerced', () => {
+  const data = [
+    { shape: '20260903', color: 'blue', value: 1 },
+    { shape: '1700000000000', color: 'blue', value: 2 },
+  ];
+  const props = buildDefaultProps({
+    data,
+    rows: ['color'],
+    cols: ['shape'],
+    tableOptions: { dateFormatters: { shape: getTimeFormatter('%Y-%m-%d') } },
+  });
+  renderWithTheme(<TableRenderer {...props} />);
+
+  // "20260903" is a YYYYMMDD date key, not an epoch offset. Coercing it to a
+  // number would render it as 1970-01-01; a real stringified epoch still
+  // coerces.
+  expect(screen.getByText('20260903')).toBeInTheDocument();
+  expect(screen.getByText('2023-11-14')).toBeInTheDocument();
+  expect(screen.queryByText('1970-01-01')).not.toBeInTheDocument();
+});
+
 test('TableRenderer applies cellColorFormatters background and contrast color to column headers', () => {
   const cellColorFormatters = {
     shape: [