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: [