diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index de72dd4fe90f9..dee7ccb8b90f9 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -220,19 +220,27 @@ def lookup_obj(lookup_column): def dttm_sql_literal(self, dttm: DateTime) -> str: """Convert datetime object to a SQL expression string""" + sql = ( + self.table.database.db_engine_spec.convert_dttm(self.type, dttm) + if self.type + else None + ) + + if sql: + return sql + tf = self.python_date_format + if tf: - seconds_since_epoch = int(dttm.timestamp()) - if tf == "epoch_s": - return str(seconds_since_epoch) - elif tf == "epoch_ms": + if tf in ["epoch_ms", "epoch_s"]: + seconds_since_epoch = int(dttm.timestamp()) + if tf == "epoch_s": + return str(seconds_since_epoch) return str(seconds_since_epoch * 1000) - return "'{}'".format(dttm.strftime(tf)) - else: - s = self.table.database.db_engine_spec.convert_dttm(self.type or "", dttm) + return f"'{dttm.strftime(tf)}'" - # TODO(john-bodley): SIP-15 will explicitly require a type conversion. - return s or "'{}'".format(dttm.strftime("%Y-%m-%d %H:%M:%S.%f")) + # TODO(john-bodley): SIP-15 will explicitly require a type conversion. + return f"""'{dttm.strftime("%Y-%m-%d %H:%M:%S.%f")}'""" class SqlMetric(Model, BaseMetric): diff --git a/superset/db_engine_specs/athena.py b/superset/db_engine_specs/athena.py index 8213bdb43acde..aec6c45e4b1b1 100644 --- a/superset/db_engine_specs/athena.py +++ b/superset/db_engine_specs/athena.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. from datetime import datetime +from typing import Optional from superset.db_engine_specs.base import BaseEngineSpec @@ -39,13 +40,13 @@ class AthenaEngineSpec(BaseEngineSpec): } @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: tt = target_type.upper() if tt == "DATE": - return "from_iso8601_date('{}')".format(dttm.isoformat()[:10]) + return f"from_iso8601_date('{dttm.date().isoformat()}')" if tt == "TIMESTAMP": - return "from_iso8601_timestamp('{}')".format(dttm.isoformat()) - return "CAST ('{}' AS TIMESTAMP)".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) + return f"""from_iso8601_timestamp('{dttm.isoformat(timespec="microseconds")}')""" # pylint: disable=line-too-long + return None @classmethod def epoch_to_dttm(cls) -> str: diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py index 97e214f42f820..2939f39522fd0 100644 --- a/superset/db_engine_specs/base.py +++ b/superset/db_engine_specs/base.py @@ -439,15 +439,15 @@ def _allowed_file(filename: str) -> bool: db.session.commit() @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: """ - Convert DateTime object to sql expression + Convert Python datetime object to a SQL expression - :param target_type: Target type of expression - :param dttm: DateTime object - :return: SQL expression + :param target_type: The target type of expression + :param dttm: The datetime object + :return: The SQL expression """ - return "'{}'".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) + return None @classmethod def get_all_datasource_names( diff --git a/superset/db_engine_specs/bigquery.py b/superset/db_engine_specs/bigquery.py index db9f4e565c545..1eace5f06768e 100644 --- a/superset/db_engine_specs/bigquery.py +++ b/superset/db_engine_specs/bigquery.py @@ -17,7 +17,7 @@ import hashlib import re from datetime import datetime -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Optional, Tuple import pandas as pd from sqlalchemy import literal_column @@ -72,11 +72,15 @@ class BigQueryEngineSpec(BaseEngineSpec): } @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: tt = target_type.upper() if tt == "DATE": - return "'{}'".format(dttm.strftime("%Y-%m-%d")) - return "'{}'".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) + return f"CAST('{dttm.date().isoformat()}' AS DATE)" + if tt == "DATETIME": + return f"""CAST('{dttm.isoformat(timespec="microseconds")}' AS DATETIME)""" + if tt == "TIMESTAMP": + return f"""CAST('{dttm.isoformat(timespec="microseconds")}' AS TIMESTAMP)""" + return None @classmethod def fetch_data(cls, cursor, limit: int) -> List[Tuple]: diff --git a/superset/db_engine_specs/clickhouse.py b/superset/db_engine_specs/clickhouse.py index e5bfdbf609aaf..b9d1ba0c2edb2 100644 --- a/superset/db_engine_specs/clickhouse.py +++ b/superset/db_engine_specs/clickhouse.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. from datetime import datetime +from typing import Optional from superset.db_engine_specs.base import BaseEngineSpec @@ -43,10 +44,10 @@ class ClickHouseEngineSpec(BaseEngineSpec): # pylint: disable=abstract-method } @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: tt = target_type.upper() if tt == "DATE": - return "toDate('{}')".format(dttm.strftime("%Y-%m-%d")) + return f"toDate('{dttm.date().isoformat()}')" if tt == "DATETIME": - return "toDateTime('{}')".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) - return "'{}'".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) + return f"""toDateTime('{dttm.isoformat(sep=" ", timespec="seconds")}')""" + return None diff --git a/superset/db_engine_specs/db2.py b/superset/db_engine_specs/db2.py index faab33f0658a8..794f987af5c2d 100644 --- a/superset/db_engine_specs/db2.py +++ b/superset/db_engine_specs/db2.py @@ -14,8 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from datetime import datetime - from superset.db_engine_specs.base import BaseEngineSpec, LimitMethod @@ -51,7 +49,3 @@ class Db2EngineSpec(BaseEngineSpec): @classmethod def epoch_to_dttm(cls) -> str: return "(TIMESTAMP('1970-01-01', '00:00:00') + {col} SECONDS)" - - @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: - return "'{}'".format(dttm.strftime("%Y-%m-%d-%H.%M.%S")) diff --git a/superset/db_engine_specs/drill.py b/superset/db_engine_specs/drill.py index ce3e57302b24d..f8277eeb7a256 100644 --- a/superset/db_engine_specs/drill.py +++ b/superset/db_engine_specs/drill.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. from datetime import datetime +from typing import Optional from urllib import parse from superset.db_engine_specs.base import BaseEngineSpec @@ -49,13 +50,13 @@ def epoch_ms_to_dttm(cls) -> str: return "TO_DATE({col})" @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: tt = target_type.upper() if tt == "DATE": - return "CAST('{}' AS DATE)".format(dttm.isoformat()[:10]) + return f"TO_DATE('{dttm.date().isoformat()}', 'yyyy-MM-dd')" elif tt == "TIMESTAMP": - return "CAST('{}' AS TIMESTAMP)".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) - return "'{}'".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) + return f"""TO_TIMESTAMP('{dttm.isoformat(sep=" ", timespec="seconds")}', 'yyyy-MM-dd HH:mm:ss')""" # pylint: disable=line-too-long + return None @classmethod def adjust_database_uri(cls, uri, selected_schema): diff --git a/superset/db_engine_specs/elasticsearch.py b/superset/db_engine_specs/elasticsearch.py index 7a016d6f7e226..5ea06ba6b07a0 100644 --- a/superset/db_engine_specs/elasticsearch.py +++ b/superset/db_engine_specs/elasticsearch.py @@ -16,7 +16,7 @@ # under the License. # pylint: disable=C,R,W from datetime import datetime -from typing import Dict +from typing import Dict, Optional from superset.db_engine_specs.base import BaseEngineSpec @@ -41,7 +41,7 @@ class ElasticSearchEngineSpec(BaseEngineSpec): type_code_map: Dict[int, str] = {} # loaded from get_datatype only if needed @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: - if target_type.upper() in ("DATETIME", "DATE"): - return f"'{dttm.isoformat()}'" - return f"'{dttm.strftime('%Y-%m-%d %H:%M:%S')}'" + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: + if target_type.upper() == "DATETIME": + return f"""CAST('{dttm.isoformat(timespec="seconds")}' AS DATETIME)""" + return None diff --git a/superset/db_engine_specs/hive.py b/superset/db_engine_specs/hive.py index dbcb337fe9bd6..c826a309020c2 100644 --- a/superset/db_engine_specs/hive.py +++ b/superset/db_engine_specs/hive.py @@ -179,13 +179,13 @@ def convert_to_hive_type(col_type): engine.execute(sql) @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: tt = target_type.upper() if tt == "DATE": - return "CAST('{}' AS DATE)".format(dttm.isoformat()[:10]) + return f"CAST('{dttm.date().isoformat()}' AS DATE)" elif tt == "TIMESTAMP": - return "CAST('{}' AS TIMESTAMP)".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) - return "'{}'".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) + return f"""CAST('{dttm.isoformat(sep=" ", timespec="microseconds")}' AS TIMESTAMP)""" # pylint: disable=line-too-long + return None @classmethod def adjust_database_uri(cls, uri, selected_schema=None): diff --git a/superset/db_engine_specs/impala.py b/superset/db_engine_specs/impala.py index 6fba983928b43..31b02301167b8 100644 --- a/superset/db_engine_specs/impala.py +++ b/superset/db_engine_specs/impala.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. from datetime import datetime -from typing import List +from typing import List, Optional from sqlalchemy.engine.reflection import Inspector @@ -43,11 +43,13 @@ def epoch_to_dttm(cls) -> str: return "from_unixtime({col})" @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: tt = target_type.upper() if tt == "DATE": - return "'{}'".format(dttm.strftime("%Y-%m-%d")) - return "'{}'".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) + return f"CAST('{dttm.date().isoformat()}' AS DATE)" + elif tt == "TIMESTAMP": + return f"""CAST('{dttm.isoformat(timespec="microseconds")}' AS TIMESTAMP)""" + return None @classmethod def get_schema_names(cls, inspector: Inspector) -> List[str]: diff --git a/superset/db_engine_specs/kylin.py b/superset/db_engine_specs/kylin.py index acea219185029..e7879cf8ac5c8 100644 --- a/superset/db_engine_specs/kylin.py +++ b/superset/db_engine_specs/kylin.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. from datetime import datetime +from typing import Optional from superset.db_engine_specs.base import BaseEngineSpec @@ -39,10 +40,10 @@ class KylinEngineSpec(BaseEngineSpec): # pylint: disable=abstract-method } @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: tt = target_type.upper() if tt == "DATE": - return "CAST('{}' AS DATE)".format(dttm.isoformat()[:10]) + return f"CAST('{dttm.date().isoformat()}' AS DATE)" if tt == "TIMESTAMP": - return "CAST('{}' AS TIMESTAMP)".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) - return "'{}'".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) + return f"""CAST('{dttm.isoformat(sep=" ", timespec="seconds")}' AS TIMESTAMP)""" # pylint: disable=line-too-long + return None diff --git a/superset/db_engine_specs/mssql.py b/superset/db_engine_specs/mssql.py index 1ce1f3a961635..c774077f612ab 100644 --- a/superset/db_engine_specs/mssql.py +++ b/superset/db_engine_specs/mssql.py @@ -50,8 +50,15 @@ def epoch_to_dttm(cls): return "dateadd(S, {col}, '1970-01-01')" @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: - return "CONVERT(DATETIME, '{}', 126)".format(dttm.isoformat()) + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: + tt = target_type.upper() + if tt == "DATE": + return f"CONVERT(DATE, '{dttm.date().isoformat()}', 23)" + if tt == "DATETIME": + return f"""CONVERT(DATETIME, '{dttm.isoformat(timespec="milliseconds")}', 126)""" # pylint: disable=line-too-long + if tt == "SMALLDATETIME": + return f"""CONVERT(SMALLDATETIME, '{dttm.isoformat(sep=" ", timespec="seconds")}', 20)""" # pylint: disable=line-too-long + return None @classmethod def fetch_data(cls, cursor, limit: int) -> List[Tuple]: diff --git a/superset/db_engine_specs/mysql.py b/superset/db_engine_specs/mysql.py index 361f4ecca8442..81dc40dca1bf8 100644 --- a/superset/db_engine_specs/mysql.py +++ b/superset/db_engine_specs/mysql.py @@ -50,12 +50,13 @@ class MySQLEngineSpec(BaseEngineSpec): type_code_map: Dict[int, str] = {} # loaded from get_datatype only if needed @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: - if target_type.upper() in ("DATETIME", "DATE"): - return "STR_TO_DATE('{}', '%Y-%m-%d %H:%i:%s')".format( - dttm.strftime("%Y-%m-%d %H:%M:%S") - ) - return "'{}'".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: + tt = target_type.upper() + if tt == "DATE": + return f"STR_TO_DATE('{dttm.date().isoformat()}', '%Y-%m-%d')" + if tt == "DATETIME": + return f"""STR_TO_DATE('{dttm.isoformat(sep=" ", timespec="microseconds")}', '%Y-%m-%d %H:%i:%s.%f')""" # pylint: disable=line-too-long + return None @classmethod def adjust_database_uri(cls, uri, selected_schema=None): diff --git a/superset/db_engine_specs/oracle.py b/superset/db_engine_specs/oracle.py index b200e45db923c..2a20d4b870b32 100644 --- a/superset/db_engine_specs/oracle.py +++ b/superset/db_engine_specs/oracle.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. from datetime import datetime +from typing import Optional from superset.db_engine_specs.base import LimitMethod from superset.db_engine_specs.postgres import PostgresBaseEngineSpec @@ -39,7 +40,10 @@ class OracleEngineSpec(PostgresBaseEngineSpec): } @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: - return ("""TO_TIMESTAMP('{}', 'YYYY-MM-DD"T"HH24:MI:SS.ff6')""").format( - dttm.isoformat() - ) + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: + tt = target_type.upper() + if tt == "DATE": + return f"TO_DATE('{dttm.date().isoformat()}', 'YYYY-MM-DD')" + if tt == "TIMESTAMP": + return f"""TO_TIMESTAMP('{dttm.isoformat(timespec="microseconds")}', 'YYYY-MM-DD"T"HH24:MI:SS.ff6')""" # pylint: disable=line-too-long + return None diff --git a/superset/db_engine_specs/postgres.py b/superset/db_engine_specs/postgres.py index bda62f39a9445..56bdc1403b349 100644 --- a/superset/db_engine_specs/postgres.py +++ b/superset/db_engine_specs/postgres.py @@ -55,10 +55,6 @@ def fetch_data(cls, cursor, limit: int) -> List[Tuple]: def epoch_to_dttm(cls) -> str: return "(timestamp 'epoch' + {col} * interval '1 second')" - @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: - return "'{}'".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) - class PostgresEngineSpec(PostgresBaseEngineSpec): engine = "postgresql" @@ -73,3 +69,12 @@ def get_table_names( tables = inspector.get_table_names(schema) tables.extend(inspector.get_foreign_table_names(schema)) return sorted(tables) + + @classmethod + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: + tt = target_type.upper() + if tt == "DATE": + return f"TO_DATE('{dttm.date().isoformat()}', 'YYYY-MM-DD')" + if tt == "TIMESTAMP": + return f"""TO_TIMESTAMP('{dttm.isoformat(sep=" ", timespec="microseconds")}', 'YYYY-MM-DD HH24:MI:SS.US')""" # pylint: disable=line-too-long + return None diff --git a/superset/db_engine_specs/presto.py b/superset/db_engine_specs/presto.py index 66e798234d674..ffe0de3ab136d 100644 --- a/superset/db_engine_specs/presto.py +++ b/superset/db_engine_specs/presto.py @@ -520,13 +520,13 @@ def adjust_database_uri(cls, uri, selected_schema=None): return uri @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: tt = target_type.upper() if tt == "DATE": - return "from_iso8601_date('{}')".format(dttm.isoformat()[:10]) + return f"""from_iso8601_date('{dttm.date().isoformat()}')""" if tt == "TIMESTAMP": - return "from_iso8601_timestamp('{}')".format(dttm.isoformat()) - return "'{}'".format(dttm.strftime("%Y-%m-%d %H:%M:%S")) + return f"""from_iso8601_timestamp('{dttm.isoformat(timespec="microseconds")}')""" # pylint: disable=line-too-long + return None @classmethod def epoch_to_dttm(cls) -> str: diff --git a/superset/db_engine_specs/sqlite.py b/superset/db_engine_specs/sqlite.py index 16b890adc2ca9..8444ede4589b7 100644 --- a/superset/db_engine_specs/sqlite.py +++ b/superset/db_engine_specs/sqlite.py @@ -75,11 +75,10 @@ def get_all_datasource_names( raise Exception(f"Unsupported datasource_type: {datasource_type}") @classmethod - def convert_dttm(cls, target_type: str, dttm: datetime) -> str: - iso = dttm.isoformat().replace("T", " ") - if "." not in iso: - iso += ".000000" - return "'{}'".format(iso) + def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: + if target_type.upper() == "TEXT": + return f"""'{dttm.isoformat(sep=" ", timespec="microseconds")}'""" + return None @classmethod def get_table_names( diff --git a/tests/db_engine_specs/athena_tests.py b/tests/db_engine_specs/athena_tests.py new file mode 100644 index 0000000000000..81b790b35b226 --- /dev/null +++ b/tests/db_engine_specs/athena_tests.py @@ -0,0 +1,33 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from superset.db_engine_specs.athena import AthenaEngineSpec +from tests.db_engine_specs.base_tests import DbEngineSpecTestCase + + +class AthenaTestCase(DbEngineSpecTestCase): + def test_convert_dttm(self): + dttm = self.get_dttm() + + self.assertEqual( + AthenaEngineSpec.convert_dttm("DATE", dttm), + "from_iso8601_date('2019-01-02')", + ) + + self.assertEqual( + AthenaEngineSpec.convert_dttm("TIMESTAMP", dttm), + "from_iso8601_timestamp('2019-01-02T03:04:05.678900')", + ) diff --git a/tests/db_engine_specs/base_engine_spec_tests.py b/tests/db_engine_specs/base_engine_spec_tests.py index 13f7b67865ed3..c595792886c46 100644 --- a/tests/db_engine_specs/base_engine_spec_tests.py +++ b/tests/db_engine_specs/base_engine_spec_tests.py @@ -202,3 +202,7 @@ def test_column_datatype_to_string(self): else: expected = ["VARCHAR(255)", "VARCHAR(255)", "FLOAT"] self.assertEqual(col_names, expected) + + def test_convert_dttm(self): + dttm = self.get_dttm() + self.assertIsNone(BaseEngineSpec.convert_dttm("", dttm)) diff --git a/tests/db_engine_specs/base_tests.py b/tests/db_engine_specs/base_tests.py index 812e6b832bbe9..d7a57ee8b8ea7 100644 --- a/tests/db_engine_specs/base_tests.py +++ b/tests/db_engine_specs/base_tests.py @@ -14,6 +14,8 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +from datetime import datetime + from superset.db_engine_specs.mysql import MySQLEngineSpec from superset.models.core import Database from tests.base_tests import SupersetTestCase @@ -26,3 +28,6 @@ def sql_limit_regex( main = Database(database_name="test_database", sqlalchemy_uri="sqlite://") limited = engine_spec_class.apply_limit_to_sql(sql, limit, main) self.assertEqual(expected_sql, limited) + + def get_dttm(self): + return datetime.strptime("2019-01-02 03:04:05.678900", "%Y-%m-%d %H:%M:%S.%f") diff --git a/tests/db_engine_specs/bigquery_tests.py b/tests/db_engine_specs/bigquery_tests.py index ec23e86b84220..9c77970d55115 100644 --- a/tests/db_engine_specs/bigquery_tests.py +++ b/tests/db_engine_specs/bigquery_tests.py @@ -37,3 +37,20 @@ def test_bigquery_sqla_column_label(self): label = BigQueryEngineSpec.make_label_compatible(column("12345_col").name) label_expected = "_12345_col_8d390" self.assertEqual(label, label_expected) + + def test_convert_dttm(self): + dttm = self.get_dttm() + + self.assertEqual( + BigQueryEngineSpec.convert_dttm("DATE", dttm), "CAST('2019-01-02' AS DATE)" + ) + + self.assertEqual( + BigQueryEngineSpec.convert_dttm("DATETIME", dttm), + "CAST('2019-01-02T03:04:05.678900' AS DATETIME)", + ) + + self.assertEqual( + BigQueryEngineSpec.convert_dttm("TIMESTAMP", dttm), + "CAST('2019-01-02T03:04:05.678900' AS TIMESTAMP)", + ) diff --git a/tests/db_engine_specs/clickhouse_tests.py b/tests/db_engine_specs/clickhouse_tests.py new file mode 100644 index 0000000000000..5c06a395959d6 --- /dev/null +++ b/tests/db_engine_specs/clickhouse_tests.py @@ -0,0 +1,32 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from superset.db_engine_specs.clickhouse import ClickHouseEngineSpec +from tests.db_engine_specs.base_tests import DbEngineSpecTestCase + + +class ClickHouseTestCase(DbEngineSpecTestCase): + def test_convert_dttm(self): + dttm = self.get_dttm() + + self.assertEqual( + ClickHouseEngineSpec.convert_dttm("DATE", dttm), "toDate('2019-01-02')" + ) + + self.assertEqual( + ClickHouseEngineSpec.convert_dttm("DATETIME", dttm), + "toDateTime('2019-01-02 03:04:05')", + ) diff --git a/tests/db_engine_specs/drill_tests.py b/tests/db_engine_specs/drill_tests.py new file mode 100644 index 0000000000000..0e64f027708b2 --- /dev/null +++ b/tests/db_engine_specs/drill_tests.py @@ -0,0 +1,33 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from superset.db_engine_specs.drill import DrillEngineSpec +from tests.db_engine_specs.base_tests import DbEngineSpecTestCase + + +class DrillTestCase(DbEngineSpecTestCase): + def test_convert_dttm(self): + dttm = self.get_dttm() + + self.assertEqual( + DrillEngineSpec.convert_dttm("DATE", dttm), + "TO_DATE('2019-01-02', 'yyyy-MM-dd')", + ) + + self.assertEqual( + DrillEngineSpec.convert_dttm("TIMESTAMP", dttm), + "TO_TIMESTAMP('2019-01-02 03:04:05', 'yyyy-MM-dd HH:mm:ss')", + ) diff --git a/tests/db_engine_specs/elasticsearch_tests.py b/tests/db_engine_specs/elasticsearch_tests.py new file mode 100644 index 0000000000000..8b222a67a0740 --- /dev/null +++ b/tests/db_engine_specs/elasticsearch_tests.py @@ -0,0 +1,28 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from superset.db_engine_specs.elasticsearch import ElasticSearchEngineSpec +from tests.db_engine_specs.base_tests import DbEngineSpecTestCase + + +class ElasticSearchTestCase(DbEngineSpecTestCase): + def test_convert_dttm(self): + dttm = self.get_dttm() + + self.assertEqual( + ElasticSearchEngineSpec.convert_dttm("DATETIME", dttm), + "CAST('2019-01-02T03:04:05' AS DATETIME)", + ) diff --git a/tests/db_engine_specs/hive_tests.py b/tests/db_engine_specs/hive_tests.py index 94a474deb22d6..4d24c0bdf25af 100644 --- a/tests/db_engine_specs/hive_tests.py +++ b/tests/db_engine_specs/hive_tests.py @@ -150,3 +150,15 @@ def test_hive_get_view_names_return_empty_list( self.assertEqual( [], HiveEngineSpec.get_view_names(mock.ANY, mock.ANY, mock.ANY) ) + + def test_convert_dttm(self): + dttm = self.get_dttm() + + self.assertEqual( + HiveEngineSpec.convert_dttm("DATE", dttm), "CAST('2019-01-02' AS DATE)" + ) + + self.assertEqual( + HiveEngineSpec.convert_dttm("TIMESTAMP", dttm), + "CAST('2019-01-02 03:04:05.678900' AS TIMESTAMP)", + ) diff --git a/tests/db_engine_specs/impala_tests.py b/tests/db_engine_specs/impala_tests.py new file mode 100644 index 0000000000000..c0de4f25de3d3 --- /dev/null +++ b/tests/db_engine_specs/impala_tests.py @@ -0,0 +1,32 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from superset.db_engine_specs.impala import ImpalaEngineSpec +from tests.db_engine_specs.base_tests import DbEngineSpecTestCase + + +class ImpalaTestCase(DbEngineSpecTestCase): + def test_convert_dttm(self): + dttm = self.get_dttm() + + self.assertEqual( + ImpalaEngineSpec.convert_dttm("DATE", dttm), "CAST('2019-01-02' AS DATE)" + ) + + self.assertEqual( + ImpalaEngineSpec.convert_dttm("TIMESTAMP", dttm), + "CAST('2019-01-02T03:04:05.678900' AS TIMESTAMP)", + ) diff --git a/tests/db_engine_specs/kylin_tests.py b/tests/db_engine_specs/kylin_tests.py new file mode 100644 index 0000000000000..2301e30eee904 --- /dev/null +++ b/tests/db_engine_specs/kylin_tests.py @@ -0,0 +1,32 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from superset.db_engine_specs.kylin import KylinEngineSpec +from tests.db_engine_specs.base_tests import DbEngineSpecTestCase + + +class KylinTestCase(DbEngineSpecTestCase): + def test_convert_dttm(self): + dttm = self.get_dttm() + + self.assertEqual( + KylinEngineSpec.convert_dttm("DATE", dttm), "CAST('2019-01-02' AS DATE)" + ) + + self.assertEqual( + KylinEngineSpec.convert_dttm("TIMESTAMP", dttm), + "CAST('2019-01-02 03:04:05' AS TIMESTAMP)", + ) diff --git a/tests/db_engine_specs/mssql_tests.py b/tests/db_engine_specs/mssql_tests.py index 989fa8c5a5afb..fabdee139ba71 100644 --- a/tests/db_engine_specs/mssql_tests.py +++ b/tests/db_engine_specs/mssql_tests.py @@ -69,3 +69,21 @@ def test_time_exp_mixd_case_col_1y(self): expr = MssqlEngineSpec.get_timestamp_expr(col, None, "P1Y") result = str(expr.compile(None, dialect=mssql.dialect())) self.assertEqual(result, "DATEADD(year, DATEDIFF(year, 0, [MixedCase]), 0)") + + def test_convert_dttm(self): + dttm = self.get_dttm() + + self.assertEqual( + MssqlEngineSpec.convert_dttm("DATE", dttm), + "CONVERT(DATE, '2019-01-02', 23)", + ) + + self.assertEqual( + MssqlEngineSpec.convert_dttm("DATETIME", dttm), + "CONVERT(DATETIME, '2019-01-02T03:04:05.678', 126)", + ) + + self.assertEqual( + MssqlEngineSpec.convert_dttm("SMALLDATETIME", dttm), + "CONVERT(SMALLDATETIME, '2019-01-02 03:04:05', 20)", + ) diff --git a/tests/db_engine_specs/mysql_tests.py b/tests/db_engine_specs/mysql_tests.py index 22205a8f5f656..d47e7072b295a 100644 --- a/tests/db_engine_specs/mysql_tests.py +++ b/tests/db_engine_specs/mysql_tests.py @@ -28,3 +28,16 @@ def test_get_datatype_mysql(self): """Tests related to datatype mapping for MySQL""" self.assertEqual("TINY", MySQLEngineSpec.get_datatype(1)) self.assertEqual("VARCHAR", MySQLEngineSpec.get_datatype(15)) + + def test_convert_dttm(self): + dttm = self.get_dttm() + + self.assertEqual( + MySQLEngineSpec.convert_dttm("DATE", dttm), + "STR_TO_DATE('2019-01-02', '%Y-%m-%d')", + ) + + self.assertEqual( + MySQLEngineSpec.convert_dttm("DATETIME", dttm), + "STR_TO_DATE('2019-01-02 03:04:05.678900', '%Y-%m-%d %H:%i:%s.%f')", + ) diff --git a/tests/db_engine_specs/oracle_tests.py b/tests/db_engine_specs/oracle_tests.py index 285f61639687c..9e394059fc0be 100644 --- a/tests/db_engine_specs/oracle_tests.py +++ b/tests/db_engine_specs/oracle_tests.py @@ -34,3 +34,17 @@ def test_oracle_time_expression_reserved_keyword_1m_grain(self): expr = OracleEngineSpec.get_timestamp_expr(col, None, "P1M") result = str(expr.compile(dialect=oracle.dialect())) self.assertEqual(result, "TRUNC(CAST(\"decimal\" as DATE), 'MONTH')") + dttm = self.get_dttm() + + def test_convert_dttm(self): + dttm = self.get_dttm() + + self.assertEqual( + OracleEngineSpec.convert_dttm("DATE", dttm), + "TO_DATE('2019-01-02', 'YYYY-MM-DD')", + ) + + self.assertEqual( + OracleEngineSpec.convert_dttm("TIMESTAMP", dttm), + """TO_TIMESTAMP('2019-01-02T03:04:05.678900', 'YYYY-MM-DD"T"HH24:MI:SS.ff6')""", + ) diff --git a/tests/db_engine_specs/postgres_tests.py b/tests/db_engine_specs/postgres_tests.py index 3204c53431110..d8b1b540c46a3 100644 --- a/tests/db_engine_specs/postgres_tests.py +++ b/tests/db_engine_specs/postgres_tests.py @@ -70,3 +70,16 @@ def test_time_exp_mixd_case_col_1y(self): expr = PostgresEngineSpec.get_timestamp_expr(col, None, "P1Y") result = str(expr.compile(None, dialect=postgresql.dialect())) self.assertEqual(result, "DATE_TRUNC('year', \"MixedCase\")") + + def test_convert_dttm(self): + dttm = self.get_dttm() + + self.assertEqual( + PostgresEngineSpec.convert_dttm("DATE", dttm), + "TO_DATE('2019-01-02', 'YYYY-MM-DD')", + ) + + self.assertEqual( + PostgresEngineSpec.convert_dttm("TIMESTAMP", dttm), + "TO_TIMESTAMP('2019-01-02 03:04:05.678900', 'YYYY-MM-DD HH24:MI:SS.US')", + ) diff --git a/tests/db_engine_specs/presto_tests.py b/tests/db_engine_specs/presto_tests.py index b72731063d096..48ad18a244503 100644 --- a/tests/db_engine_specs/presto_tests.py +++ b/tests/db_engine_specs/presto_tests.py @@ -341,3 +341,16 @@ def test_presto_where_latest_partition(self): ) query_result = str(result.compile(compile_kwargs={"literal_binds": True})) self.assertEqual("SELECT \nWHERE ds = '01-01-19' AND hour = 1", query_result) + + def test_convert_dttm(self): + dttm = self.get_dttm() + + self.assertEqual( + PrestoEngineSpec.convert_dttm("DATE", dttm), + "from_iso8601_date('2019-01-02')", + ) + + self.assertEqual( + PrestoEngineSpec.convert_dttm("TIMESTAMP", dttm), + "from_iso8601_timestamp('2019-01-02T03:04:05.678900')", + ) diff --git a/tests/db_engine_specs/sqlite_tests.py b/tests/db_engine_specs/sqlite_tests.py new file mode 100644 index 0000000000000..71104c47d1555 --- /dev/null +++ b/tests/db_engine_specs/sqlite_tests.py @@ -0,0 +1,27 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from superset.db_engine_specs.sqlite import SqliteEngineSpec +from tests.db_engine_specs.base_tests import DbEngineSpecTestCase + + +class SQliteTestCase(DbEngineSpecTestCase): + def test_convert_dttm(self): + dttm = self.get_dttm() + + self.assertEqual( + SqliteEngineSpec.convert_dttm("TEXT", dttm), "'2019-01-02 03:04:05.678900'" + )