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")