diff --git a/superset/db_engine_specs/virtuoso.py b/superset/db_engine_specs/virtuoso.py new file mode 100644 index 0000000000000..5fda7fc2e73a1 --- /dev/null +++ b/superset/db_engine_specs/virtuoso.py @@ -0,0 +1,95 @@ +# 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 datetime import datetime +from typing import Any, Optional + +from sqlalchemy import types + +from superset.constants import TimeGrain +from superset.db_engine_specs.base import BaseEngineSpec, LimitMethod + + +class VirtuosoEngineSpec(BaseEngineSpec): + """Engine for Virtuoso""" + + engine = "virtuoso" + engine_name = "Virtuoso" + + sqlalchemy_uri_placeholder = ( + "virtuoso+pyodbc://user:password@dsn" + ) + + # Virtuoso uses FIRST to limit: `SELECT FIRST 10 * FROM table` + limit_method = LimitMethod.FETCH_MANY + max_column_name_length = 100 + # supports_catalog = True + disable_ssh_tunneling = True + + _time_grain_expressions = { + None: "{col}", + TimeGrain.SECOND: ( + "CAST(CAST({col} AS DATE) " + "|| ' ' " + "|| EXTRACT(HOUR FROM {col}) " + "|| ':' " + "|| EXTRACT(MINUTE FROM {col}) " + "|| ':' " + "|| FLOOR(EXTRACT(SECOND FROM {col})) AS TIMESTAMP)" + ), + TimeGrain.MINUTE: ( + "CAST(CAST({col} AS DATE) " + "|| ' ' " + "|| EXTRACT(HOUR FROM {col}) " + "|| ':' " + "|| EXTRACT(MINUTE FROM {col}) " + "|| ':00' AS TIMESTAMP)" + ), + TimeGrain.HOUR: ( + "CAST(CAST({col} AS DATE) " + "|| ' ' " + "|| EXTRACT(HOUR FROM {col}) " + "|| ':00:00' AS TIMESTAMP)" + ), + TimeGrain.DAY: "CAST({col} AS DATE)", + TimeGrain.MONTH: ( + "CAST(EXTRACT(YEAR FROM {col}) " + "|| '-' " + "|| EXTRACT(MONTH FROM {col}) " + "|| '-01' AS DATE)" + ), + TimeGrain.YEAR: "CAST(EXTRACT(YEAR FROM {col}) || '-01-01' AS DATE)", + } + + @classmethod + def epoch_to_dttm(cls) -> str: + return "DATEADD('second', {col}, stringdate('1970-01-01'))" + + @classmethod + def convert_dttm( + cls, target_type: str, dttm: datetime, db_extra: Optional[dict[str, Any]] = None + ) -> Optional[str]: + sqla_type = cls.get_sqla_column_type(target_type) + + if isinstance(sqla_type, types.Date): + return f"CAST('{dttm.date().isoformat()}' AS DATE)" + if isinstance(sqla_type, types.DateTime): + dttm_formatted = dttm.isoformat(sep=" ") + dttm_valid_precision = dttm_formatted[: len("YYYY-MM-DD HH:MM:SS.MMMM")] + return f"CAST('{dttm_valid_precision}' AS TIMESTAMP)" + if isinstance(sqla_type, types.Time): + return f"CAST('{dttm.time().isoformat()}' AS TIME)" + return None