You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by dp...@apache.org on 2021/02/02 15:08:29 UTC
[superset] branch master updated: refactor: dbapi exception mapping
for dbapi's (#12869)
This is an automated email from the ASF dual-hosted git repository.
dpgaspar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 6c018c0 refactor: dbapi exception mapping for dbapi's (#12869)
6c018c0 is described below
commit 6c018c0a28831472c3b7e988038121fe9dd13792
Author: Daniel Vaz Gaspar <da...@gmail.com>
AuthorDate: Tue Feb 2 15:07:46 2021 +0000
refactor: dbapi exception mapping for dbapi's (#12869)
* refactor: dbapi exception mapping for dbapi's
* fix test
* fix lint
* fix grammar on comment
---
superset/db_engine_specs/base.py | 44 ++++++++++++++++++++--
superset/db_engine_specs/clickhouse.py | 18 ++++++++-
superset/db_engine_specs/elasticsearch.py | 17 ++++++++-
.../db_engine_specs/exceptions.py | 33 ++++++++++------
tests/db_engine_specs/clickhouse_tests.py | 15 ++++++++
5 files changed, 109 insertions(+), 18 deletions(-)
diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py
index aa52abf..1e560e6 100644
--- a/superset/db_engine_specs/base.py
+++ b/superset/db_engine_specs/base.py
@@ -32,6 +32,7 @@ from typing import (
Optional,
Pattern,
Tuple,
+ Type,
TYPE_CHECKING,
Union,
)
@@ -178,6 +179,35 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
}
@classmethod
+ def get_dbapi_exception_mapping(cls) -> Dict[Type[Exception], Type[Exception]]:
+ """
+ Each engine can implement and converge its own specific exceptions into
+ Superset DBAPI exceptions
+
+ Note: On python 3.9 this method can be changed to a classmethod property
+ without the need of implementing a metaclass type
+
+ :return: A map of driver specific exception to superset custom exceptions
+ """
+ return {}
+
+ @classmethod
+ def get_dbapi_mapped_exception(cls, exception: Exception) -> Exception:
+ """
+ Get a superset custom DBAPI exception from the driver specific exception.
+
+ Override if the engine needs to perform extra changes to the exception, for
+ example change the exception message or implement custom more complex logic
+
+ :param exception: The driver specific exception
+ :return: Superset custom DBAPI exception
+ """
+ new_exception = cls.get_dbapi_exception_mapping().get(type(exception))
+ if not new_exception:
+ return exception
+ return new_exception(str(exception))
+
+ @classmethod
def is_db_column_type_match(
cls, db_column_type: Optional[str], target_column_type: utils.GenericDataType
) -> bool:
@@ -314,9 +344,12 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
"""
if cls.arraysize:
cursor.arraysize = cls.arraysize
- if cls.limit_method == LimitMethod.FETCH_MANY and limit:
- return cursor.fetchmany(limit)
- return cursor.fetchall()
+ try:
+ if cls.limit_method == LimitMethod.FETCH_MANY and limit:
+ return cursor.fetchmany(limit)
+ return cursor.fetchall()
+ except Exception as ex:
+ raise cls.get_dbapi_mapped_exception(ex)
@classmethod
def expand_data(
@@ -902,7 +935,10 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
"""
if cls.arraysize:
cursor.arraysize = cls.arraysize
- cursor.execute(query)
+ try:
+ cursor.execute(query)
+ except Exception as ex:
+ raise cls.get_dbapi_mapped_exception(ex)
@classmethod
def make_label_compatible(cls, label: str) -> Union[str, quoted_name]:
diff --git a/superset/db_engine_specs/clickhouse.py b/superset/db_engine_specs/clickhouse.py
index 658aef0..4db5684 100644
--- a/superset/db_engine_specs/clickhouse.py
+++ b/superset/db_engine_specs/clickhouse.py
@@ -15,9 +15,10 @@
# specific language governing permissions and limitations
# under the License.
from datetime import datetime
-from typing import Optional
+from typing import Dict, Optional, Type
from superset.db_engine_specs.base import BaseEngineSpec
+from superset.db_engine_specs.exceptions import SupersetDBAPIDatabaseError
from superset.utils import core as utils
@@ -46,6 +47,21 @@ class ClickHouseEngineSpec(BaseEngineSpec): # pylint: disable=abstract-method
}
@classmethod
+ def get_dbapi_exception_mapping(cls) -> Dict[Type[Exception], Type[Exception]]:
+ from urllib3.exceptions import NewConnectionError
+
+ return {NewConnectionError: SupersetDBAPIDatabaseError}
+
+ @classmethod
+ def get_dbapi_mapped_exception(cls, exception: Exception) -> Exception:
+ new_exception = cls.get_dbapi_exception_mapping().get(type(exception))
+ if new_exception == SupersetDBAPIDatabaseError:
+ return SupersetDBAPIDatabaseError("Connection failed")
+ if not new_exception:
+ return exception
+ return new_exception(str(exception))
+
+ @classmethod
def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]:
tt = target_type.upper()
if tt == utils.TemporalType.DATE:
diff --git a/superset/db_engine_specs/elasticsearch.py b/superset/db_engine_specs/elasticsearch.py
index 4f60677..2d52d3c 100644
--- a/superset/db_engine_specs/elasticsearch.py
+++ b/superset/db_engine_specs/elasticsearch.py
@@ -15,9 +15,14 @@
# specific language governing permissions and limitations
# under the License.
from datetime import datetime
-from typing import Dict, Optional
+from typing import Dict, Optional, Type
from superset.db_engine_specs.base import BaseEngineSpec
+from superset.db_engine_specs.exceptions import (
+ SupersetDBAPIDatabaseError,
+ SupersetDBAPIOperationalError,
+ SupersetDBAPIProgrammingError,
+)
from superset.utils import core as utils
@@ -42,6 +47,16 @@ class ElasticSearchEngineSpec(BaseEngineSpec): # pylint: disable=abstract-metho
type_code_map: Dict[int, str] = {} # loaded from get_datatype only if needed
@classmethod
+ def get_dbapi_exception_mapping(cls) -> Dict[Type[Exception], Type[Exception]]:
+ import es.exceptions as es_exceptions # pylint: disable=import-error
+
+ return {
+ es_exceptions.DatabaseError: SupersetDBAPIDatabaseError,
+ es_exceptions.OperationalError: SupersetDBAPIOperationalError,
+ es_exceptions.ProgrammingError: SupersetDBAPIProgrammingError,
+ }
+
+ @classmethod
def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]:
if target_type.upper() == utils.TemporalType.DATETIME:
return f"""CAST('{dttm.isoformat(timespec="seconds")}' AS DATETIME)"""
diff --git a/tests/db_engine_specs/clickhouse_tests.py b/superset/db_engine_specs/exceptions.py
similarity index 60%
copy from tests/db_engine_specs/clickhouse_tests.py
copy to superset/db_engine_specs/exceptions.py
index b6416c8..6b4fb55 100644
--- a/tests/db_engine_specs/clickhouse_tests.py
+++ b/superset/db_engine_specs/exceptions.py
@@ -14,19 +14,28 @@
# 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 TestDbEngineSpec
+from superset.exceptions import SupersetException
-class TestClickHouseDbEngineSpec(TestDbEngineSpec):
- def test_convert_dttm(self):
- dttm = self.get_dttm()
+class SupersetDBAPIError(SupersetException):
+ pass
- 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')",
- )
+class SupersetDBAPIDataError(SupersetDBAPIError):
+ pass
+
+
+class SupersetDBAPIDatabaseError(SupersetDBAPIError):
+ pass
+
+
+class SupersetDBAPIDisconnectionError(SupersetDBAPIError):
+ pass
+
+
+class SupersetDBAPIOperationalError(SupersetDBAPIError):
+ pass
+
+
+class SupersetDBAPIProgrammingError(SupersetDBAPIError):
+ pass
diff --git a/tests/db_engine_specs/clickhouse_tests.py b/tests/db_engine_specs/clickhouse_tests.py
index b6416c8..c6f506c 100644
--- a/tests/db_engine_specs/clickhouse_tests.py
+++ b/tests/db_engine_specs/clickhouse_tests.py
@@ -14,7 +14,12 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+from unittest import mock
+
+import pytest
+
from superset.db_engine_specs.clickhouse import ClickHouseEngineSpec
+from superset.db_engine_specs.exceptions import SupersetDBAPIDatabaseError
from tests.db_engine_specs.base_tests import TestDbEngineSpec
@@ -30,3 +35,13 @@ class TestClickHouseDbEngineSpec(TestDbEngineSpec):
ClickHouseEngineSpec.convert_dttm("DATETIME", dttm),
"toDateTime('2019-01-02 03:04:05')",
)
+
+ def test_execute_connection_error(self):
+ from urllib3.exceptions import NewConnectionError
+
+ cursor = mock.Mock()
+ cursor.execute.side_effect = NewConnectionError(
+ "Dummypool", message="Exception with sensitive data"
+ )
+ with pytest.raises(SupersetDBAPIDatabaseError) as ex:
+ ClickHouseEngineSpec.execute(cursor, "SELECT col1 from table1")