diff --git a/docs/src/pages/docs/Miscellaneous/issue_codes.mdx b/docs/src/pages/docs/Miscellaneous/issue_codes.mdx index 36e4df21468..34e5b130688 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 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. diff --git a/superset-frontend/src/components/ErrorMessage/types.ts b/superset-frontend/src/components/ErrorMessage/types.ts index 5ae5e7f85dd..9a164e91291 100644 --- a/superset-frontend/src/components/ErrorMessage/types.ts +++ b/superset-frontend/src/components/ErrorMessage/types.ts @@ -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', diff --git a/superset-frontend/src/setup/setupErrorMessages.ts b/superset-frontend/src/setup/setupErrorMessages.ts index aebebc34fb3..50d1fb501b8 100644 --- a/superset-frontend/src/setup/setupErrorMessages.ts +++ b/superset-frontend/src/setup/setupErrorMessages.ts @@ -79,5 +79,9 @@ export default function setupErrorMessages() { 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 3205d08afa6..e4236cfb3c5 100644 --- a/superset/databases/commands/exceptions.py +++ b/superset/databases/commands/exceptions.py @@ -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") diff --git a/superset/databases/commands/test_connection.py b/superset/databases/commands/test_connection.py index 024763060cf..11c221953a5 100644 --- a/superset/databases/commands/test_connection.py +++ b/superset/databases/commands/test_connection.py @@ -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") diff --git a/superset/db_engine_specs/mysql.py b/superset/db_engine_specs/mysql.py index e9b46b634fd..b72b006c601 100644 --- a/superset/db_engine_specs/mysql.py +++ b/superset/db_engine_specs/mysql.py @@ -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, ), } diff --git a/superset/db_engine_specs/postgres.py b/superset/db_engine_specs/postgres.py index 1dc91a99312..92c00013f2a 100644 --- a/superset/db_engine_specs/postgres.py +++ b/superset/db_engine_specs/postgres.py @@ -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, ), } diff --git a/superset/db_engine_specs/presto.py b/superset/db_engine_specs/presto.py index a62dd0ecfa7..c6cec6a5467 100644 --- a/superset/db_engine_specs/presto.py +++ b/superset/db_engine_specs/presto.py @@ -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.+?): .*Column '(?P.+?)' cannot be resolved" +) +TABLE_DOES_NOT_EXIST_REGEX = re.compile(".*Table (?P.+?) does not exist") +SCHEMA_DOES_NOT_EXIST_REGEX = re.compile( + "line (?P.+?): .*Schema '(?P.+?)' 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.+?): Catalog '(?P.+?)' 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""" diff --git a/superset/errors.py b/superset/errors.py index f4845aa288b..901ad8cfc12 100644 --- a/superset/errors.py +++ b/superset/errors.py @@ -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: [ { diff --git a/tests/db_engine_specs/mysql_tests.py b/tests/db_engine_specs/mysql_tests.py index fff54e1cd53..b40fd281bbe 100644 --- a/tests/db_engine_specs/mysql_tests.py +++ b/tests/db_engine_specs/mysql_tests.py @@ -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.", } ], }, diff --git a/tests/db_engine_specs/postgres_tests.py b/tests/db_engine_specs/postgres_tests.py index 2121e929e93..0f4461aad01 100644 --- a/tests/db_engine_specs/postgres_tests.py +++ b/tests/db_engine_specs/postgres_tests.py @@ -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.", + ), } ], }, diff --git a/tests/db_engine_specs/presto_tests.py b/tests/db_engine_specs/presto_tests.py index 38b159453cc..336c01e5dee 100644 --- a/tests/db_engine_specs/presto_tests.py +++ b/tests/db_engine_specs/presto_tests.py @@ -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: