feat: error messages for Presto connections  (#14172)

* chore: rename connection errors

* feat: error messages for Presto connections

* Add unit tests

* Update docs/src/pages/docs/Miscellaneous/issue_codes.mdx

Co-authored-by: AAfghahi <48933336+AAfghahi@users.noreply.github.com>

Co-authored-by: AAfghahi <48933336+AAfghahi@users.noreply.github.com>
diff --git a/docs/src/pages/docs/Miscellaneous/issue_codes.mdx b/docs/src/pages/docs/Miscellaneous/issue_codes.mdx
index 36e4df2..34e5b13 100644
--- a/docs/src/pages/docs/Miscellaneous/issue_codes.mdx
+++ b/docs/src/pages/docs/Miscellaneous/issue_codes.mdx
@@ -166,3 +166,12 @@
 ```
 
 Either the database was written incorrectly or it does not exist. Check that it was typed correctly.
+
+
+## Issue 1016
+
+```
+The schema was deleted or renamed in the database.
+```
+
+The schema was either removed or renamed. Check that the schema is typed correctly and exists.
diff --git a/superset-frontend/src/components/ErrorMessage/types.ts b/superset-frontend/src/components/ErrorMessage/types.ts
index 5ae5e7f..9a164e9 100644
--- a/superset-frontend/src/components/ErrorMessage/types.ts
+++ b/superset-frontend/src/components/ErrorMessage/types.ts
@@ -28,6 +28,7 @@
   GENERIC_DB_ENGINE_ERROR: 'GENERIC_DB_ENGINE_ERROR',
   COLUMN_DOES_NOT_EXIST_ERROR: 'COLUMN_DOES_NOT_EXIST_ERROR',
   TABLE_DOES_NOT_EXIST_ERROR: 'TABLE_DOES_NOT_EXIST_ERROR',
+  SCHEMA_DOES_NOT_EXIST_ERROR: 'SCHEMA_DOES_NOT_EXIST_ERROR',
   CONNECTION_INVALID_USERNAME_ERROR: 'CONNECTION_INVALID_USERNAME_ERROR',
   CONNECTION_INVALID_PASSWORD_ERROR: 'CONNECTION_INVALID_PASSWORD_ERROR',
   CONNECTION_INVALID_HOSTNAME_ERROR: 'CONNECTION_INVALID_HOSTNAME_ERROR',
diff --git a/superset-frontend/src/setup/setupErrorMessages.ts b/superset-frontend/src/setup/setupErrorMessages.ts
index aebebc3..50d1fb5 100644
--- a/superset-frontend/src/setup/setupErrorMessages.ts
+++ b/superset-frontend/src/setup/setupErrorMessages.ts
@@ -79,5 +79,9 @@
     ErrorTypeEnum.CONNECTION_UNKNOWN_DATABASE_ERROR,
     DatabaseErrorMessage,
   );
+  errorMessageComponentRegistry.registerValue(
+    ErrorTypeEnum.SCHEMA_DOES_NOT_EXIST_ERROR,
+    DatabaseErrorMessage,
+  );
   setupErrorMessagesExtra();
 }
diff --git a/superset/databases/commands/exceptions.py b/superset/databases/commands/exceptions.py
index 3205d08..e4236cf 100644
--- a/superset/databases/commands/exceptions.py
+++ b/superset/databases/commands/exceptions.py
@@ -130,7 +130,8 @@
     message = _("Could not load database driver")
 
 
-class DatabaseTestConnectionUnexpectedError(CommandInvalidError):
+class DatabaseTestConnectionUnexpectedError(SupersetErrorsException):
+    status = 422
     message = _("Unexpected error occurred, please check your logs for details")
 
 
diff --git a/superset/databases/commands/test_connection.py b/superset/databases/commands/test_connection.py
index 0247630..11c2219 100644
--- a/superset/databases/commands/test_connection.py
+++ b/superset/databases/commands/test_connection.py
@@ -49,6 +49,17 @@
         uri = self._properties.get("sqlalchemy_uri", "")
         if self._model and uri == self._model.safe_sqlalchemy_uri():
             uri = self._model.sqlalchemy_uri_decrypted
+
+        # context for error messages
+        url = make_url(uri)
+        context = {
+            "hostname": url.host,
+            "password": url.password,
+            "port": url.port,
+            "username": url.username,
+            "database": url.database,
+        }
+
         try:
             database = DatabaseDAO.build_db_for_connection_test(
                 server_cert=self._properties.get("server_cert", ""),
@@ -87,14 +98,6 @@
                 engine=database.db_engine_spec.__name__,
             )
             # check for custom errors (wrong username, wrong password, etc)
-            url = make_url(uri)
-            context = {
-                "hostname": url.host,
-                "password": url.password,
-                "port": url.port,
-                "username": url.username,
-                "database": url.database,
-            }
             errors = database.db_engine_spec.extract_errors(ex, context)
             raise DatabaseTestConnectionFailedError(errors)
         except SupersetSecurityException as ex:
@@ -108,7 +111,8 @@
                 action=f"test_connection_error.{ex.__class__.__name__}",
                 engine=database.db_engine_spec.__name__,
             )
-            raise DatabaseTestConnectionUnexpectedError(str(ex))
+            errors = database.db_engine_spec.extract_errors(ex, context)
+            raise DatabaseTestConnectionUnexpectedError(errors)
 
     def validate(self) -> None:
         database_name = self._properties.get("database_name")
diff --git a/superset/db_engine_specs/mysql.py b/superset/db_engine_specs/mysql.py
index e9b46b6..b72b006 100644
--- a/superset/db_engine_specs/mysql.py
+++ b/superset/db_engine_specs/mysql.py
@@ -121,10 +121,7 @@
             SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
         ),
         CONNECTION_UNKNOWN_DATABASE_REGEX: (
-            __(
-                'We were unable to connect to your database named "%(database)s". '
-                "Please verify your database name and try again."
-            ),
+            __('Unable to connect to database "%(database)s".'),
             SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR,
         ),
     }
diff --git a/superset/db_engine_specs/postgres.py b/superset/db_engine_specs/postgres.py
index 1dc91a9..92c0001 100644
--- a/superset/db_engine_specs/postgres.py
+++ b/superset/db_engine_specs/postgres.py
@@ -124,10 +124,7 @@
             SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
         ),
         CONNECTION_UNKNOWN_DATABASE_REGEX: (
-            __(
-                'We were unable to connect to your database named "%(database)s".'
-                " Please verify your database name and try again."
-            ),
+            __('Unable to connect to database "%(database)s".'),
             SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR,
         ),
     }
diff --git a/superset/db_engine_specs/presto.py b/superset/db_engine_specs/presto.py
index a62dd0e..c6cec6a 100644
--- a/superset/db_engine_specs/presto.py
+++ b/superset/db_engine_specs/presto.py
@@ -51,7 +51,7 @@
 
 from superset import app, cache_manager, is_feature_enabled
 from superset.db_engine_specs.base import BaseEngineSpec
-from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
+from superset.errors import SupersetErrorType
 from superset.exceptions import SupersetTemplateException
 from superset.models.sql_lab import Query
 from superset.models.sql_types.presto_sql_types import (
@@ -70,8 +70,28 @@
     # prevent circular imports
     from superset.models.core import Database
 
-COLUMN_NOT_RESOLVED_ERROR_REGEX = "line (.+?): .*Column '(.+?)' cannot be resolved"
-TABLE_DOES_NOT_EXIST_ERROR_REGEX = ".*Table (.+?) does not exist"
+COLUMN_DOES_NOT_EXIST_REGEX = re.compile(
+    "line (?P<location>.+?): .*Column '(?P<column_name>.+?)' cannot be resolved"
+)
+TABLE_DOES_NOT_EXIST_REGEX = re.compile(".*Table (?P<table_name>.+?) does not exist")
+SCHEMA_DOES_NOT_EXIST_REGEX = re.compile(
+    "line (?P<location>.+?): .*Schema '(?P<schema_name>.+?)' does not exist"
+)
+CONNECTION_ACCESS_DENIED_REGEX = re.compile("Access Denied: Invalid credentials")
+CONNECTION_INVALID_HOSTNAME_REGEX = re.compile(
+    r"Failed to establish a new connection: \[Errno 8\] nodename nor servname "
+    "provided, or not known"
+)
+CONNECTION_HOST_DOWN_REGEX = re.compile(
+    r"Failed to establish a new connection: \[Errno 60\] Operation timed out"
+)
+CONNECTION_PORT_CLOSED_REGEX = re.compile(
+    r"Failed to establish a new connection: \[Errno 61\] Connection refused"
+)
+CONNECTION_UNKNOWN_DATABASE_ERROR = re.compile(
+    r"line (?P<location>.+?): Catalog '(?P<catalog_name>.+?)' does not exist"
+)
+
 
 QueryStatus = utils.QueryStatus
 config = app.config
@@ -145,6 +165,53 @@
         "date_add('day', 1, CAST({col} AS TIMESTAMP))))",
     }
 
+    custom_errors = {
+        COLUMN_DOES_NOT_EXIST_REGEX: (
+            __(
+                'We can\'t seem to resolve the column "%(column_name)s" at '
+                "line %(location)s.",
+            ),
+            SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR,
+        ),
+        TABLE_DOES_NOT_EXIST_REGEX: (
+            __(
+                'The table "%(table_name)s" does not exist. '
+                "A valid table must be used to run this query.",
+            ),
+            SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR,
+        ),
+        SCHEMA_DOES_NOT_EXIST_REGEX: (
+            __(
+                'The schema "%(schema_name)s" does not exist. '
+                "A valid schema must be used to run this query.",
+            ),
+            SupersetErrorType.SCHEMA_DOES_NOT_EXIST_ERROR,
+        ),
+        CONNECTION_ACCESS_DENIED_REGEX: (
+            __('Either the username "%(username)s" or the password is incorrect.'),
+            SupersetErrorType.CONNECTION_ACCESS_DENIED_ERROR,
+        ),
+        CONNECTION_INVALID_HOSTNAME_REGEX: (
+            __('The hostname "%(hostname)s" cannot be resolved.'),
+            SupersetErrorType.CONNECTION_INVALID_HOSTNAME_ERROR,
+        ),
+        CONNECTION_HOST_DOWN_REGEX: (
+            __(
+                'The host "%(hostname)s" might be down, and can\'t be '
+                "reached on port %(port)s."
+            ),
+            SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
+        ),
+        CONNECTION_PORT_CLOSED_REGEX: (
+            __('Port %(port)s on hostname "%(hostname)s" refused the connection.'),
+            SupersetErrorType.CONNECTION_PORT_CLOSED_ERROR,
+        ),
+        CONNECTION_UNKNOWN_DATABASE_ERROR: (
+            __('Unable to connect to catalog named "%(catalog_name)s".'),
+            SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR,
+        ),
+    }
+
     @classmethod
     def get_allow_cost_estimate(cls, extra: Dict[str, Any]) -> bool:
         version = extra.get("version")
@@ -1132,45 +1199,6 @@
         return database.get_df("SHOW FUNCTIONS")["Function"].tolist()
 
     @classmethod
-    def extract_errors(
-        cls, ex: Exception, context: Optional[Dict[str, Any]] = None
-    ) -> List[SupersetError]:
-        raw_message = cls._extract_error_message(ex)
-
-        column_match = re.search(COLUMN_NOT_RESOLVED_ERROR_REGEX, raw_message)
-        if column_match:
-            return [
-                SupersetError(
-                    error_type=SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR,
-                    message=__(
-                        'We can\'t seem to resolve the column "%(column_name)s" at '
-                        "line %(location)s.",
-                        column_name=column_match.group(2),
-                        location=column_match.group(1),
-                    ),
-                    level=ErrorLevel.ERROR,
-                    extra={"engine_name": cls.engine_name},
-                )
-            ]
-
-        table_match = re.search(TABLE_DOES_NOT_EXIST_ERROR_REGEX, raw_message)
-        if table_match:
-            return [
-                SupersetError(
-                    error_type=SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR,
-                    message=__(
-                        'The table "%(table_name)s" does not exist. '
-                        "A valid table must be used to run this query.",
-                        table_name=table_match.group(1),
-                    ),
-                    level=ErrorLevel.ERROR,
-                    extra={"engine_name": cls.engine_name},
-                )
-            ]
-
-        return super().extract_errors(ex, context)
-
-    @classmethod
     def is_readonly_query(cls, parsed_query: ParsedQuery) -> bool:
         """Pessimistic readonly, 100% sure statement won't mutate anything"""
         return super().is_readonly_query(parsed_query) or parsed_query.is_show()
diff --git a/superset/errors.py b/superset/errors.py
index f4845aa..901ad8c 100644
--- a/superset/errors.py
+++ b/superset/errors.py
@@ -39,6 +39,7 @@
     GENERIC_DB_ENGINE_ERROR = "GENERIC_DB_ENGINE_ERROR"
     COLUMN_DOES_NOT_EXIST_ERROR = "COLUMN_DOES_NOT_EXIST_ERROR"
     TABLE_DOES_NOT_EXIST_ERROR = "TABLE_DOES_NOT_EXIST_ERROR"
+    SCHEMA_DOES_NOT_EXIST_ERROR = "SCHEMA_DOES_NOT_EXIST_ERROR"
     CONNECTION_INVALID_USERNAME_ERROR = "CONNECTION_INVALID_USERNAME_ERROR"
     CONNECTION_INVALID_PASSWORD_ERROR = "CONNECTION_INVALID_PASSWORD_ERROR"
     CONNECTION_INVALID_HOSTNAME_ERROR = "CONNECTION_INVALID_HOSTNAME_ERROR"
@@ -116,6 +117,21 @@
             ),
         },
     ],
+    SupersetErrorType.SCHEMA_DOES_NOT_EXIST_ERROR: [
+        {
+            "code": 1003,
+            "message": _(
+                "Issue 1003 - There is a syntax error in the SQL query. "
+                "Perhaps there was a misspelling or a typo."
+            ),
+        },
+        {
+            "code": 1016,
+            "message": _(
+                "Issue 1005 - The schema was deleted or renamed in the database."
+            ),
+        },
+    ],
     SupersetErrorType.MISSING_TEMPLATE_PARAMS_ERROR: [
         {
             "code": 1006,
@@ -132,7 +148,7 @@
         },
     ],
     SupersetErrorType.CONNECTION_PORT_CLOSED_ERROR: [
-        {"code": 1008, "message": _("Issue 1008 - The port is closed."),},
+        {"code": 1008, "message": _("Issue 1008 - The port is closed.")},
     ],
     SupersetErrorType.CONNECTION_HOST_DOWN_ERROR: [
         {
diff --git a/tests/db_engine_specs/mysql_tests.py b/tests/db_engine_specs/mysql_tests.py
index fff54e1..b40fd28 100644
--- a/tests/db_engine_specs/mysql_tests.py
+++ b/tests/db_engine_specs/mysql_tests.py
@@ -200,17 +200,15 @@
         result = MySQLEngineSpec.extract_errors(Exception(msg))
         assert result == [
             SupersetError(
+                message='Unable to connect to database "badDB".',
                 error_type=SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR,
-                message='We were unable to connect to your database named "badDB".'
-                " Please verify your database name and try again.",
                 level=ErrorLevel.ERROR,
                 extra={
                     "engine_name": "MySQL",
                     "issue_codes": [
                         {
-                            "code": 10015,
-                            "message": "Issue 1015 - Either the database is "
-                            "spelled incorrectly or does not exist.",
+                            "code": 1015,
+                            "message": "Issue 1015 - Either the database is spelled incorrectly or does not exist.",
                         }
                     ],
                 },
diff --git a/tests/db_engine_specs/postgres_tests.py b/tests/db_engine_specs/postgres_tests.py
index 2121e92..0f4461a 100644
--- a/tests/db_engine_specs/postgres_tests.py
+++ b/tests/db_engine_specs/postgres_tests.py
@@ -371,17 +371,18 @@
         result = PostgresEngineSpec.extract_errors(Exception(msg))
         assert result == [
             SupersetError(
+                message='Unable to connect to database "badDB".',
                 error_type=SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR,
-                message='We were unable to connect to your database named "badDB".'
-                " Please verify your database name and try again.",
                 level=ErrorLevel.ERROR,
                 extra={
                     "engine_name": "PostgreSQL",
                     "issue_codes": [
                         {
-                            "code": 10015,
-                            "message": "Issue 1015 - Either the database is "
-                            "spelled incorrectly or does not exist.",
+                            "code": 1015,
+                            "message": (
+                                "Issue 1015 - Either the database is spelled "
+                                "incorrectly or does not exist.",
+                            ),
                         }
                     ],
                 },
diff --git a/tests/db_engine_specs/presto_tests.py b/tests/db_engine_specs/presto_tests.py
index 38b1594..336c01e 100644
--- a/tests/db_engine_specs/presto_tests.py
+++ b/tests/db_engine_specs/presto_tests.py
@@ -23,6 +23,7 @@
 from sqlalchemy.sql import select
 
 from superset.db_engine_specs.presto import PrestoEngineSpec
+from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
 from superset.sql_parse import ParsedQuery
 from superset.utils.core import DatasourceName, GenericDataType
 from tests.db_engine_specs.base_tests import TestDbEngineSpec
@@ -829,6 +830,193 @@
         result = PrestoEngineSpec._extract_error_message(exception)
         assert result == "Err message"
 
+    def test_extract_errors(self):
+        msg = "Generic Error"
+        result = PrestoEngineSpec.extract_errors(Exception(msg))
+        assert result == [
+            SupersetError(
+                message="Generic Error",
+                error_type=SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
+                level=ErrorLevel.ERROR,
+                extra={
+                    "engine_name": "Presto",
+                    "issue_codes": [
+                        {
+                            "code": 1002,
+                            "message": "Issue 1002 - The database returned an unexpected error.",
+                        }
+                    ],
+                },
+            )
+        ]
+
+        msg = "line 1:8: Column 'bogus' cannot be resolved"
+        result = PrestoEngineSpec.extract_errors(Exception(msg))
+        assert result == [
+            SupersetError(
+                message='We can\'t seem to resolve the column "bogus" at line 1:8.',
+                error_type=SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR,
+                level=ErrorLevel.ERROR,
+                extra={
+                    "engine_name": "Presto",
+                    "issue_codes": [
+                        {
+                            "code": 1003,
+                            "message": "Issue 1003 - There is a syntax error in the SQL query. Perhaps there was a misspelling or a typo.",
+                        },
+                        {
+                            "code": 1004,
+                            "message": "Issue 1004 - The column was deleted or renamed in the database.",
+                        },
+                    ],
+                },
+            )
+        ]
+
+        msg = "line 1:15: Table 'tpch.tiny.region2' does not exist"
+        result = PrestoEngineSpec.extract_errors(Exception(msg))
+        assert result == [
+            SupersetError(
+                message="The table \"'tpch.tiny.region2'\" does not exist. A valid table must be used to run this query.",
+                error_type=SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR,
+                level=ErrorLevel.ERROR,
+                extra={
+                    "engine_name": "Presto",
+                    "issue_codes": [
+                        {
+                            "code": 1003,
+                            "message": "Issue 1003 - There is a syntax error in the SQL query. Perhaps there was a misspelling or a typo.",
+                        },
+                        {
+                            "code": 1005,
+                            "message": "Issue 1005 - The table was deleted or renamed in the database.",
+                        },
+                    ],
+                },
+            )
+        ]
+
+        msg = "line 1:15: Schema 'tin' does not exist"
+        result = PrestoEngineSpec.extract_errors(Exception(msg))
+        assert result == [
+            SupersetError(
+                message='The schema "tin" does not exist. A valid schema must be used to run this query.',
+                error_type=SupersetErrorType.SCHEMA_DOES_NOT_EXIST_ERROR,
+                level=ErrorLevel.ERROR,
+                extra={
+                    "engine_name": "Presto",
+                    "issue_codes": [
+                        {
+                            "code": 1003,
+                            "message": "Issue 1003 - There is a syntax error in the SQL query. Perhaps there was a misspelling or a typo.",
+                        },
+                        {
+                            "code": 1016,
+                            "message": "Issue 1005 - The schema was deleted or renamed in the database.",
+                        },
+                    ],
+                },
+            )
+        ]
+
+        msg = b"Access Denied: Invalid credentials"
+        result = PrestoEngineSpec.extract_errors(Exception(msg), {"username": "alice"})
+        assert result == [
+            SupersetError(
+                message='Either the username "alice" or the password is incorrect.',
+                error_type=SupersetErrorType.CONNECTION_ACCESS_DENIED_ERROR,
+                level=ErrorLevel.ERROR,
+                extra={
+                    "engine_name": "Presto",
+                    "issue_codes": [
+                        {
+                            "code": 1014,
+                            "message": "Issue 1014 - Either the username or the password is wrong.",
+                        }
+                    ],
+                },
+            )
+        ]
+
+        msg = "Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known"
+        result = PrestoEngineSpec.extract_errors(
+            Exception(msg), {"hostname": "badhost"}
+        )
+        assert result == [
+            SupersetError(
+                message='The hostname "badhost" cannot be resolved.',
+                error_type=SupersetErrorType.CONNECTION_INVALID_HOSTNAME_ERROR,
+                level=ErrorLevel.ERROR,
+                extra={
+                    "engine_name": "Presto",
+                    "issue_codes": [
+                        {
+                            "code": 1007,
+                            "message": "Issue 1007 - The hostname provided can't be resolved.",
+                        }
+                    ],
+                },
+            )
+        ]
+
+        msg = "Failed to establish a new connection: [Errno 60] Operation timed out"
+        result = PrestoEngineSpec.extract_errors(
+            Exception(msg), {"hostname": "badhost", "port": 12345}
+        )
+        assert result == [
+            SupersetError(
+                message='The host "badhost" might be down, and can\'t be reached on port 12345.',
+                error_type=SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
+                level=ErrorLevel.ERROR,
+                extra={
+                    "engine_name": "Presto",
+                    "issue_codes": [
+                        {
+                            "code": 1009,
+                            "message": "Issue 1009 - The host might be down, and can't be reached on the provided port.",
+                        }
+                    ],
+                },
+            )
+        ]
+
+        msg = "Failed to establish a new connection: [Errno 61] Connection refused"
+        result = PrestoEngineSpec.extract_errors(
+            Exception(msg), {"hostname": "badhost", "port": 12345}
+        )
+        assert result == [
+            SupersetError(
+                message='Port 12345 on hostname "badhost" refused the connection.',
+                error_type=SupersetErrorType.CONNECTION_PORT_CLOSED_ERROR,
+                level=ErrorLevel.ERROR,
+                extra={
+                    "engine_name": "Presto",
+                    "issue_codes": [
+                        {"code": 1008, "message": "Issue 1008 - The port is closed."}
+                    ],
+                },
+            )
+        ]
+
+        msg = "line 1:15: Catalog 'wrong' does not exist"
+        result = PrestoEngineSpec.extract_errors(Exception(msg))
+        assert result == [
+            SupersetError(
+                message='Unable to connect to catalog named "wrong".',
+                error_type=SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR,
+                level=ErrorLevel.ERROR,
+                extra={
+                    "engine_name": "Presto",
+                    "issue_codes": [
+                        {
+                            "code": 1015,
+                            "message": "Issue 1015 - Either the database is spelled incorrectly or does not exist.",
+                        }
+                    ],
+                },
+            )
+        ]
+
 
 def test_is_readonly():
     def is_readonly(sql: str) -> bool: