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];
     }