You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by hu...@apache.org on 2026/10/13 18:15:26 UTC
(superset) 06/07: fix(datasource): preview a formatted temporal column in its own format
This is an automated email from the ASF dual-hosted git repository.
hughhhh pushed a commit to branch hughhhh/pfm-6-explore-indicator
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 00e5f9fe12cf3884a5fd7ae8c8f74dc7c565501f
Author: Hugh A. Miles II <hu...@gmail.com>
AuthorDate: Tue Oct 13 16:41:13 2026 +0000
fix(datasource): preview a formatted temporal column in its own format
Every non-numeric temporal column got the ISO sample, whatever its
`python_date_format`. A `STRING` column flagged temporal and stored as
`%Y%m%d` compares `'20260115'` -- `dttm_sql_literal` falls back to the
format when the engine's `convert_dttm` answers nothing -- so mapping one
onto a `DATE` key with `PARSE_DATE('%Y%m%d', :value)` previewed the
transform against `'2026-01-15 00:00:00'`, a sample no chart bound
produces, and reported a working mapping as broken.
The sample instant is a constant, so the format is applied from a table of
directive values rather than by pulling in a date library: there is nothing
to compute, and a partial `strftime` that silently got a field wrong would
be worse than not rendering. A directive the table does not know falls back
to the ISO sample rather than sending a literal `%c`.
Narrowed to the columns where the format actually reaches the comparison: a
declared `TIMESTAMP`/`DATE` keeps the ISO sample, since the engine renders
its own literal, and `epoch_s`/`epoch_ms` stay with the numeric branch that
already handles them -- they are Superset sentinels, not formats, and would
otherwise have rendered literally.
Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
Signed-off-by: Hugh A. Miles II <hu...@gmail.com>
---
.../PartitionFilterMapping/utils.test.ts | 54 +++++++++++++++
.../components/PartitionFilterMapping/utils.ts | 80 +++++++++++++++++++++-
2 files changed, 132 insertions(+), 2 deletions(-)
diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/PartitionFilterMapping/utils.test.ts b/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/PartitionFilterMapping/utils.test.ts
index 7c99ae269f0..9e11a8e3ed5 100644
--- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/PartitionFilterMapping/utils.test.ts
+++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/PartitionFilterMapping/utils.test.ts
@@ -637,6 +637,60 @@ test('a temporal column stored as a number previews in its stored representation
).toEqual(['2026-01-15 00:00:00']);
});
+test('a temporal column stored as formatted text previews in that format', () => {
+ // A `STRING` column flagged temporal with `%Y%m%d` compares `'20260115'`:
+ // `dttm_sql_literal` falls back to `python_date_format` when the engine's
+ // `convert_dttm` answers nothing. Previewing the ISO timestamp evaluated the
+ // transform on a sample no chart bound produces, so a working mapping --
+ // `PARSE_DATE('%Y%m%d', :value)` onto a `DATE` key -- reported as broken.
+ expect(
+ sampleValuesFor({
+ column_name: 'ds',
+ type: 'STRING',
+ is_dttm: true,
+ python_date_format: '%Y%m%d',
+ }),
+ ).toEqual(['20260115']);
+ expect(
+ sampleValuesFor({
+ column_name: 'ds',
+ type: 'VARCHAR(20)',
+ is_dttm: true,
+ python_date_format: '%Y-%m-%d %H:%M:%S',
+ }),
+ ).toEqual(['2026-01-15 00:00:00']);
+ expect(
+ sampleValuesFor({
+ column_name: 'ds',
+ type: 'TEXT',
+ is_dttm: true,
+ python_date_format: '%d/%m/%Y',
+ }),
+ ).toEqual(['15/01/2026']);
+
+ // A declared temporal type keeps the ISO sample: the engine renders its own
+ // literal, so the format never reaches the comparison.
+ expect(
+ sampleValuesFor({
+ column_name: 'event_time',
+ type: 'TIMESTAMP',
+ is_dttm: true,
+ python_date_format: '%Y%m%d',
+ }),
+ ).toEqual(['2026-01-15 00:00:00']);
+
+ // And a directive the renderer does not know falls back rather than sending
+ // a sample with a literal `%c` in it.
+ expect(
+ sampleValuesFor({
+ column_name: 'ds',
+ type: 'STRING',
+ is_dttm: true,
+ python_date_format: '%c',
+ }),
+ ).toEqual(['2026-01-15 00:00:00']);
+});
+
test('"map a column" suggests a temporal column over whatever sorts first', () => {
// The alternative lands on `revenue`, which nobody would mirror onto a
// partition key.
diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/PartitionFilterMapping/utils.ts b/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/PartitionFilterMapping/utils.ts
index 9577115884a..5185cd1dd87 100644
--- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/PartitionFilterMapping/utils.ts
+++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/PartitionFilterMapping/utils.ts
@@ -308,6 +308,67 @@ const EPOCH_SAMPLES: Record<string, string> = {
epoch_ms: '1768435200000',
};
+/** The sample instant as ISO text, which is what an unformatted column gets. */
+const TIMESTAMP_SAMPLE = '2026-01-15 00:00:00';
+
+/**
+ * Declared types the engine renders a datetime for on its own.
+ *
+ * `python_date_format` only reaches the comparison when the engine's
+ * `convert_dttm` answers nothing (`dttm_sql_literal`), which is the case for a
+ * column flagged temporal whose *declared* type is not. A real `TIMESTAMP`
+ * column carrying a format still compares the engine's own literal, so it
+ * keeps the ISO sample.
+ */
+const TEMPORAL_TYPE_RE = /DATE|TIME|TIMESTAMP|DATETIME/i;
+
+/**
+ * Each `strftime` directive's value for the sample instant.
+ *
+ * A table rather than a date library because the instant is a constant: there
+ * is nothing to compute, and a partial `strftime` implementation that silently
+ * got a field wrong would be worse than not rendering at all.
+ */
+const STRFTIME_FIELDS: Record<string, string> = {
+ Y: '2026',
+ y: '26',
+ m: '01',
+ d: '15',
+ H: '00',
+ I: '12',
+ M: '00',
+ S: '00',
+ f: '000000',
+ j: '015',
+ p: 'AM',
+ '%': '%',
+};
+
+/**
+ * The sample instant rendered through a column's `python_date_format`.
+ *
+ * Returns `undefined` for a format carrying a directive the table above does
+ * not know, so the caller can fall back rather than send a string with a
+ * literal `%c` in it.
+ */
+function renderSampleWithFormat(format: string): string | undefined {
+ let rendered = '';
+ for (let i = 0; i < format.length; i += 1) {
+ if (format[i] !== '%') {
+ rendered += format[i];
+ continue;
+ }
+ const directive = format[i + 1];
+ const value = directive === undefined ? undefined : STRFTIME_FIELDS[directive];
+ if (value === undefined) {
+ return undefined;
+ }
+ rendered += value;
+ i += 1;
+ }
+ return rendered;
+}
+
/**
* Samples the preview evaluates the transform at.
*
@@ -333,10 +394,25 @@ export function sampleValuesFor(
): string[] {
const isNumeric = Boolean(column?.type && NUMERIC_TYPE_RE.test(column.type));
if (column?.is_dttm) {
+ const format = column.python_date_format ?? '';
if (!isNumeric) {
- return ['2026-01-15 00:00:00'];
+ // A column flagged temporal whose declared type is not reaches the probe
+ // rendered through its own format: `dttm_sql_literal` falls back to
+ // `python_date_format` when the engine's `convert_dttm` answers nothing,
+ // so a `STRING` column stored as `%Y%m%d` compares `'20260115'`.
+ // Previewing the ISO timestamp evaluated the transform on a sample no
+ // chart bound produces and reported a working mapping as broken.
+ //
+ // Only a real `strftime` pattern: `epoch_s`/`epoch_ms` are Superset's
+ // own sentinels rather than formats, and are handled below for the
+ // numeric columns that can hold them.
+ const formatted =
+ format.includes('%') && !TEMPORAL_TYPE_RE.test(column.type ?? '')
+ ? renderSampleWithFormat(format)
+ : undefined;
+ return [formatted || TIMESTAMP_SAMPLE];
}
- const epochSample = EPOCH_SAMPLES[column.python_date_format ?? ''];
+ const epochSample = EPOCH_SAMPLES[format];
if (epochSample) {
return [epochSample];
}