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>
This commit is contained in:
Beto Dealmeida
2021-04-16 12:49:47 -07:00
committed by GitHub
parent df04c3af21
commit c7112d1c48
12 changed files with 315 additions and 71 deletions

View File

@@ -166,3 +166,12 @@ Either the database is spelled incorrectly or does not exist.
```
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.

View File

@@ -28,6 +28,7 @@ export const ErrorTypeEnum = {
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',

View File

@@ -79,5 +79,9 @@ export default function setupErrorMessages() {
ErrorTypeEnum.CONNECTION_UNKNOWN_DATABASE_ERROR,
DatabaseErrorMessage,
);
errorMessageComponentRegistry.registerValue(
ErrorTypeEnum.SCHEMA_DOES_NOT_EXIST_ERROR,
DatabaseErrorMessage,
);
setupErrorMessagesExtra();
}

View File

@@ -130,7 +130,8 @@ class DatabaseTestConnectionDriverError(CommandInvalidError):
message = _("Could not load database driver")
class DatabaseTestConnectionUnexpectedError(CommandInvalidError):
class DatabaseTestConnectionUnexpectedError(SupersetErrorsException):
status = 422
message = _("Unexpected error occurred, please check your logs for details")

View File

@@ -49,6 +49,17 @@ class TestConnectionDatabaseCommand(BaseCommand):
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 @@ class TestConnectionDatabaseCommand(BaseCommand):
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 @@ class TestConnectionDatabaseCommand(BaseCommand):
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")

View File

@@ -121,10 +121,7 @@ class MySQLEngineSpec(BaseEngineSpec):
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,
),
}

View File

@@ -124,10 +124,7 @@ class PostgresBaseEngineSpec(BaseEngineSpec):
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,
),
}

View File

@@ -51,7 +51,7 @@ from sqlalchemy.types import TypeEngine
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 @@ if TYPE_CHECKING:
# 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 @@ class PrestoEngineSpec(BaseEngineSpec): # pylint: disable=too-many-public-metho
"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")
@@ -1131,45 +1198,6 @@ class PrestoEngineSpec(BaseEngineSpec): # pylint: disable=too-many-public-metho
"""
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"""

View File

@@ -39,6 +39,7 @@ class SupersetErrorType(str, Enum):
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 @@ ERROR_TYPES_TO_ISSUE_CODES_MAPPING = {
),
},
],
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 @@ ERROR_TYPES_TO_ISSUE_CODES_MAPPING = {
},
],
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: [
{

View File

@@ -200,17 +200,15 @@ class TestMySQLEngineSpecsDbEngineSpec(TestDbEngineSpec):
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.",
}
],
},

View File

@@ -371,17 +371,18 @@ psql: error: could not connect to server: Operation timed out
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.",
),
}
],
},

View File

@@ -23,6 +23,7 @@ from sqlalchemy.engine.result import RowProxy
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 @@ class TestPrestoDbEngineSpec(TestDbEngineSpec):
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: