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: