diff --git a/superset/assets/src/datasource/DatasourceEditor.jsx b/superset/assets/src/datasource/DatasourceEditor.jsx index 014be93fb3e9..10a08618b850 100644 --- a/superset/assets/src/datasource/DatasourceEditor.jsx +++ b/superset/assets/src/datasource/DatasourceEditor.jsx @@ -97,14 +97,23 @@ function ColumnCollectionTable({ - {t('The pattern of the timestamp format, use ')} + {t('The pattern of timestamp format. For strings use ')} {t('python datetime string pattern')} - {t(` expression. If time is stored in epoch format, put \`epoch_s\` or - \`epoch_ms\`.`)} + {t(' expression which needs to adhere to the ')} + + {t('ISO 8601')} + + {t(` standard to ensure that the lexicographical ordering + coincides with the chronological ordering. If the + timestamp format does not adhere to the ISO 8601 standard + you will need to define an expression and type for + transforming the string into a date or timestamp. Note + currently time zones are not supported. If time is stored + in epoch format, put \`epoch_s\` or \`epoch_ms\`.`)} } control={} diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 054fa2b3def7..fcd045e0ae26 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -219,6 +219,8 @@ def dttm_sql_literal(self, dttm: DateTime) -> str: return "'{}'".format(dttm.strftime(tf)) else: s = self.table.database.db_engine_spec.convert_dttm(self.type or "", dttm) + + # TODO(john-bodley): SIP-15 will explicitly require a type conversion. return s or "'{}'".format(dttm.strftime("%Y-%m-%d %H:%M:%S.%f")) diff --git a/superset/connectors/sqla/views.py b/superset/connectors/sqla/views.py index 3b242ec8ed4d..38145ef88586 100644 --- a/superset/connectors/sqla/views.py +++ b/superset/connectors/sqla/views.py @@ -17,6 +17,7 @@ # pylint: disable=C,R,W """Views used by the SqlAlchemy connector""" import logging +import re from flask import flash, Markup, redirect from flask_appbuilder import CompactCRUDMixin, expose @@ -27,6 +28,7 @@ from flask_babel import gettext as __ from flask_babel import lazy_gettext as _ from wtforms.ext.sqlalchemy.fields import QuerySelectField +from wtforms.validators import Regexp from superset import appbuilder, db, security_manager from superset.connectors.base.views import DatasourceModelView @@ -99,12 +101,17 @@ class TableColumnInlineView(CompactCRUDMixin, SupersetModelView): # noqa ), "python_date_format": utils.markdown( Markup( - "The pattern of timestamp format, use " + "The pattern of timestamp format. For strings use " '' - "python datetime string pattern " - "expression. If time is stored in epoch " - "format, put `epoch_s` or `epoch_ms`." + "python datetime string pattern expression which needs to " + 'adhere to the ' + "ISO 8601 standard to ensure that the lexicographical ordering " + "coincides with the chronological ordering. If the timestamp " + "format does not adhere to the ISO 8601 standard you will need to " + "define an expression and type for transforming the string into a " + "date or timestamp. Note currently time zones are not supported. " + "If time is stored in epoch format, put `epoch_s` or `epoch_ms`." ), True, ), @@ -121,6 +128,24 @@ class TableColumnInlineView(CompactCRUDMixin, SupersetModelView): # noqa "python_date_format": _("Datetime Format"), "type": _("Type"), } + validators_columns = { + "python_date_format": [ + # Restrict viable values to epoch_s, epoch_ms, or a strftime format + # which adhere's to the ISO 8601 format (without time zone). + Regexp( + re.compile( + r""" + ^( + epoch_s|epoch_ms| + (?P%Y(-%m(-%d)?)?)([\sT](?P