From 1c56857f0cb3e6a2caaa7d5031c05370e75cc033 Mon Sep 17 00:00:00 2001 From: Denodo Research Labs <65558872+denodo-research-labs@users.noreply.github.com> Date: Fri, 25 Oct 2024 01:24:28 +0200 Subject: [PATCH] feat(db_engine_specs): added support for Denodo Virtual DataPort (#29927) --- README.md | 1 + docs/docs/configuration/databases.mdx | 11 ++ docs/src/resources/data.js | 5 + docs/static/img/databases/denodo.png | Bin 0 -> 17428 bytes pyproject.toml | 1 + superset/db_engine_specs/denodo.py | 158 ++++++++++++++++++ .../unit_tests/db_engine_specs/test_denodo.py | 146 ++++++++++++++++ 7 files changed, 322 insertions(+) create mode 100644 docs/static/img/databases/denodo.png create mode 100644 superset/db_engine_specs/denodo.py create mode 100644 tests/unit_tests/db_engine_specs/test_denodo.py diff --git a/README.md b/README.md index 51540104efdf1..62d5e11ee76a8 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ Here are some of the major database solutions that are supported: doris oceanbase oceanbase + denodo

**A more comprehensive list of supported databases** along with the configuration instructions can be found [here](https://superset.apache.org/docs/configuration/databases). diff --git a/docs/docs/configuration/databases.mdx b/docs/docs/configuration/databases.mdx index b02a1a83887a5..8f69cc8d6f670 100644 --- a/docs/docs/configuration/databases.mdx +++ b/docs/docs/configuration/databases.mdx @@ -55,6 +55,7 @@ are compatible with Superset. | [ClickHouse](/docs/configuration/databases#clickhouse) | `pip install clickhouse-connect` | `clickhousedb://{username}:{password}@{hostname}:{port}/{database}` | | [CockroachDB](/docs/configuration/databases#cockroachdb) | `pip install cockroachdb` | `cockroachdb://root@{hostname}:{port}/{database}?sslmode=disable` | | [Couchbase](/docs/configuration/databases#couchbase) | `pip install couchbase-sqlalchemy` | `couchbase://{username}:{password}@{hostname}:{port}?truststorepath={ssl certificate path}` | +| [Denodo](/docs/configuration/databases#denodo) | `pip install denodo-sqlalchemy` | `denodo://{username}:{password}@{hostname}:{port}/{database}` | | [Dremio](/docs/configuration/databases#dremio) | `pip install sqlalchemy_dremio` |`dremio+flight://{username}:{password}@{host}:32010`, often useful: `?UseEncryption=true/false`. For Legacy ODBC: `dremio+pyodbc://{username}:{password}@{host}:31010` | | [Elasticsearch](/docs/configuration/databases#elasticsearch) | `pip install elasticsearch-dbapi` | `elasticsearch+http://{user}:{password}@{host}:9200/` | | [Exasol](/docs/configuration/databases#exasol) | `pip install sqlalchemy-exasol` | `exa+pyodbc://{username}:{password}@{hostname}:{port}/my_schema?CONNECTIONLCALL=en_US.UTF-8&driver=EXAODBC` | @@ -512,6 +513,16 @@ For a connection to a SQL endpoint you need to use the HTTP path from the endpoi ``` +#### Denodo + +The recommended connector library for Denodo is +[denodo-sqlalchemy](https://pypi.org/project/denodo-sqlalchemy/). + +The expected connection string is formatted as follows (default port is 9996): + +``` +denodo://{username}:{password}@{hostname}:{port}/{database} +``` #### Dremio diff --git a/docs/src/resources/data.js b/docs/src/resources/data.js index ec19e92400256..baeed74eb9543 100644 --- a/docs/src/resources/data.js +++ b/docs/src/resources/data.js @@ -132,4 +132,9 @@ export const Databases = [ href: 'https://www.couchbase.com/', imgName: 'couchbase.svg', }, + { + title: 'Denodo', + href: 'https://www.denodo.com/', + imgName: 'denodo.png', + }, ]; diff --git a/docs/static/img/databases/denodo.png b/docs/static/img/databases/denodo.png new file mode 100644 index 0000000000000000000000000000000000000000..3561c387c4bf2587ca3a9249ddac06535bffe56f GIT binary patch literal 17428 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.2.0", ] db2 = ["ibm-db-sa>0.3.8, <=0.4.0"] +denodo = ["denodo-sqlalchemy~=1.0.6"] dremio = ["sqlalchemy-dremio>=1.2.1, <4"] drill = ["sqlalchemy-drill>=1.1.4, <2"] druid = ["pydruid>=0.6.5,<0.7"] diff --git a/superset/db_engine_specs/denodo.py b/superset/db_engine_specs/denodo.py new file mode 100644 index 0000000000000..2de528260f864 --- /dev/null +++ b/superset/db_engine_specs/denodo.py @@ -0,0 +1,158 @@ +# 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. +import re +from datetime import datetime +from typing import Any, Optional + +from sqlalchemy.types import Date, DateTime + +from superset.db_engine_specs.base import BaseEngineSpec, BasicParametersMixin +from superset.errors import SupersetErrorType + + +# Internal class for defining error message patterns (for translation) +class _ErrorPatterns: # pylint: disable=too-few-public-methods + CONN_INVALID_USER_PWD_REGEX = re.compile("The username or password is incorrect") + CONN_INVALID_PWD_NEEDED_REGEX = re.compile("no password supplied") + CONN_INVALID_HOSTNAME_REGEX = re.compile( + 'could not translate host name "(?P.*?)" to address: ' + ) + CONN_PORT_CLOSED_REGEX = re.compile( + "Is the server running on that host and accepting" + ) + CONN_UNKNOWN_DATABASE_REGEX = re.compile("Database '(?P.*?)' not found") + CONN_FORBIDDEN_DATABASE_REGEX = re.compile( + "Insufficient privileges to connect to the database '(?P.*?)'" + ) + QUERY_SYNTAX_ERROR_REGEX = re.compile("Exception parsing query near '(?P.*?)'") + QUERY_COLUMN_NOT_EXIST_REGEX = re.compile( + "Field not found '(?P.*?)' in view '(?P.*?)'" + ) + QUERY_GROUPBY_ERROR_REGEX = re.compile( + "Error computing capabilities of GROUP BY view" + ) + QUERY_GROUPBY_CANT_PROJ_REGEX = re.compile( + "Invalid GROUP BY expression. '(?P.*?)' cannot be projected" + ) + + +class DenodoEngineSpec(BaseEngineSpec, BasicParametersMixin): + engine = "denodo" + engine_name = "Denodo" + + default_driver = "psycopg2" + sqlalchemy_uri_placeholder = ( + "denodo://user:password@host:port/dbname[?key=value&key=value...]" + ) + encryption_parameters = {"sslmode": "require"} + + _time_grain_expressions = { + None: "{col}", + "PT1M": "TRUNC({col},'MI')", + "PT1H": "TRUNC({col},'HH')", + "P1D": "TRUNC({col},'DDD')", + "P1W": "TRUNC({col},'W')", + "P1M": "TRUNC({col},'MONTH')", + "P3M": "TRUNC({col},'Q')", + "P1Y": "TRUNC({col},'YEAR')", + } + + custom_errors: dict[ + re.Pattern[str], tuple[str, SupersetErrorType, dict[str, Any]] + ] = { + _ErrorPatterns.CONN_INVALID_USER_PWD_REGEX: ( + "Incorrect username or password.", + SupersetErrorType.CONNECTION_INVALID_USERNAME_ERROR, + {"invalid": ["username", "password"]}, + ), + _ErrorPatterns.CONN_INVALID_PWD_NEEDED_REGEX: ( + "Please enter a password.", + SupersetErrorType.CONNECTION_ACCESS_DENIED_ERROR, + {"invalid": ["password"]}, + ), + _ErrorPatterns.CONN_INVALID_HOSTNAME_REGEX: ( + 'Hostname "%(hostname)s" cannot be resolved.', + SupersetErrorType.CONNECTION_INVALID_HOSTNAME_ERROR, + {"invalid": ["host"]}, + ), + _ErrorPatterns.CONN_PORT_CLOSED_REGEX: ( + "Server refused the connection: check hostname and port.", + SupersetErrorType.CONNECTION_PORT_CLOSED_ERROR, + {"invalid": ["host", "port"]}, + ), + _ErrorPatterns.CONN_UNKNOWN_DATABASE_REGEX: ( + 'Unable to connect to database "%(database)s"', + SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR, + {"invalid": ["database"]}, + ), + _ErrorPatterns.CONN_FORBIDDEN_DATABASE_REGEX: ( + 'Unable to connect to database "%(database)s": database does not ' + "exist or insufficient permissions", + SupersetErrorType.CONNECTION_DATABASE_PERMISSIONS_ERROR, + {"invalid": ["database"]}, + ), + _ErrorPatterns.QUERY_SYNTAX_ERROR_REGEX: ( + "Please check your query for syntax errors at or " + 'near "%(err)s". Then, try running your query again.', + SupersetErrorType.SYNTAX_ERROR, + {}, + ), + _ErrorPatterns.QUERY_COLUMN_NOT_EXIST_REGEX: ( + 'Column "%(column)s" not found in "%(view)s".', + SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR, + {}, + ), + _ErrorPatterns.QUERY_GROUPBY_ERROR_REGEX: ( + "Invalid aggregation expression.", + SupersetErrorType.SYNTAX_ERROR, + {}, + ), + _ErrorPatterns.QUERY_GROUPBY_CANT_PROJ_REGEX: ( + '"%(exp)s" is neither an aggregation function nor ' + "appears in the GROUP BY clause.", + SupersetErrorType.SYNTAX_ERROR, + {}, + ), + } + + @classmethod + def epoch_to_dttm(cls) -> str: + return "GETTIMEFROMMILLIS({col})" + + @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, Date): + return f"TO_DATE('yyyy-MM-dd', '{dttm.date().isoformat()}')" + if isinstance(sqla_type, DateTime): + dttm_formatted = dttm.isoformat(sep=" ", timespec="milliseconds") + return f"TO_TIMESTAMP('yyyy-MM-dd HH:mm:ss.SSS', '{dttm_formatted}')" + return None + + @classmethod + def get_datatype(cls, type_code: Any) -> Optional[str]: + # pylint: disable=import-outside-toplevel + from psycopg2.extensions import binary_types, string_types + + # Obtain data type names from psycopg2 + types = binary_types.copy() + types.update(string_types) + if type_code in types: + return types[type_code].name + return None diff --git a/tests/unit_tests/db_engine_specs/test_denodo.py b/tests/unit_tests/db_engine_specs/test_denodo.py new file mode 100644 index 0000000000000..31e9c0dea0d96 --- /dev/null +++ b/tests/unit_tests/db_engine_specs/test_denodo.py @@ -0,0 +1,146 @@ +# 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 + +import pytest +from sqlalchemy import column, types +from sqlalchemy.engine.url import make_url + +from superset.db_engine_specs.denodo import DenodoEngineSpec as spec +from superset.utils.core import GenericDataType +from tests.unit_tests.db_engine_specs.utils import ( + assert_column_spec, + assert_convert_dttm, +) +from tests.unit_tests.fixtures.common import dttm # noqa: F401 + + +@pytest.mark.parametrize( + "target_type,expected_result", + [ + ("Date", "TO_DATE('yyyy-MM-dd', '2019-01-02')"), + ( + "DateTime", + "TO_TIMESTAMP('yyyy-MM-dd HH:mm:ss.SSS', '2019-01-02 03:04:05.678')", + ), + ( + "TimeStamp", + "TO_TIMESTAMP('yyyy-MM-dd HH:mm:ss.SSS', '2019-01-02 03:04:05.678')", + ), + ("UnknownType", None), + ], +) +def test_convert_dttm( + target_type: str, + expected_result: Optional[str], + dttm: datetime, # noqa: F811 +) -> None: + assert_convert_dttm(spec, target_type, expected_result, dttm) + + +def test_epoch_to_dttm( + dttm: datetime, # noqa: F811 +) -> None: + assert isinstance(dttm, datetime) + assert ( + spec.epoch_to_dttm().format(col="epoch_dttm") == "GETTIMEFROMMILLIS(epoch_dttm)" + ) + + +@pytest.mark.parametrize( + "native_type,sqla_type,attrs,generic_type,is_dttm", + [ + ("SMALLINT", types.SmallInteger, None, GenericDataType.NUMERIC, False), + ("INTEGER", types.Integer, None, GenericDataType.NUMERIC, False), + ("BIGINT", types.BigInteger, None, GenericDataType.NUMERIC, False), + ("DECIMAL", types.Numeric, None, GenericDataType.NUMERIC, False), + ("NUMERIC", types.Numeric, None, GenericDataType.NUMERIC, False), + ("REAL", types.REAL, None, GenericDataType.NUMERIC, False), + ("MONEY", types.Numeric, None, GenericDataType.NUMERIC, False), + # String + ("CHAR", types.String, None, GenericDataType.STRING, False), + ("VARCHAR", types.String, None, GenericDataType.STRING, False), + ("TEXT", types.String, None, GenericDataType.STRING, False), + # Temporal + ("DATE", types.Date, None, GenericDataType.TEMPORAL, True), + ("TIMESTAMP", types.TIMESTAMP, None, GenericDataType.TEMPORAL, True), + ("TIME", types.Time, None, GenericDataType.TEMPORAL, True), + # Boolean + ("BOOLEAN", types.Boolean, None, GenericDataType.BOOLEAN, False), + ], +) +def test_get_column_spec( + native_type: str, + sqla_type: type[types.TypeEngine], + attrs: Optional[dict[str, Any]], + generic_type: GenericDataType, + is_dttm: bool, +) -> None: + assert_column_spec(spec, native_type, sqla_type, attrs, generic_type, is_dttm) + + +def test_get_schema_from_engine_params() -> None: + """ + Test the ``get_schema_from_engine_params`` method. + Should return None. + """ + + assert ( + spec.get_schema_from_engine_params( + make_url("denodo://user:password@host/db"), {} + ) + is None + ) + + +def test_get_default_catalog() -> None: + """ + Test ``get_default_catalog``. + Should return None. + """ + from superset.models.core import Database + + database = Database( + database_name="denodo", + sqlalchemy_uri="denodo://user:password@host:9996/db", + ) + assert spec.get_default_catalog(database) is None + + +@pytest.mark.parametrize( + "time_grain,expected_result", + [ + (None, "col"), + ("PT1M", "TRUNC(col,'MI')"), + ("PT1H", "TRUNC(col,'HH')"), + ("P1D", "TRUNC(col,'DDD')"), + ("P1W", "TRUNC(col,'W')"), + ("P1M", "TRUNC(col,'MONTH')"), + ("P3M", "TRUNC(col,'Q')"), + ("P1Y", "TRUNC(col,'YEAR')"), + ], +) +def test_timegrain_expressions(time_grain: str, expected_result: str) -> None: + """ + DB Eng Specs (denodo): Test time grain expressions + """ + actual = str( + spec.get_timestamp_expr(col=column("col"), pdf=None, time_grain=time_grain) + ) + assert actual == expected_result