diff --git a/superset/db_engine_specs/athena.py b/superset/db_engine_specs/athena.py index d82591756a00..2952d8267772 100644 --- a/superset/db_engine_specs/athena.py +++ b/superset/db_engine_specs/athena.py @@ -14,12 +14,20 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +import re from datetime import datetime -from typing import Optional +from typing import Any, Dict, Optional, Pattern, Tuple + +from flask_babel import gettext as __ from superset.db_engine_specs.base import BaseEngineSpec +from superset.errors import SupersetErrorType from superset.utils import core as utils +SYNTAX_ERROR_REGEX = re.compile( + ": mismatched input '(?P.*?)'. Expecting: " +) + class AthenaEngineSpec(BaseEngineSpec): engine = "awsathena" @@ -41,6 +49,17 @@ class AthenaEngineSpec(BaseEngineSpec): date_add('day', 1, CAST({col} AS TIMESTAMP))))", } + custom_errors: Dict[Pattern[str], Tuple[str, SupersetErrorType, Dict[str, Any]]] = { + SYNTAX_ERROR_REGEX: ( + __( + "Please check your query for syntax errors at or " + 'near "%(syntax_error)s". Then, try running your query again.' + ), + SupersetErrorType.SYNTAX_ERROR, + {}, + ), + } + @classmethod def convert_dttm(cls, target_type: str, dttm: datetime) -> Optional[str]: tt = target_type.upper() diff --git a/superset/db_engine_specs/gsheets.py b/superset/db_engine_specs/gsheets.py index f715cd43b332..1773a3db5cce 100644 --- a/superset/db_engine_specs/gsheets.py +++ b/superset/db_engine_specs/gsheets.py @@ -14,12 +14,17 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from typing import Optional +import re +from typing import Any, Dict, Optional, Pattern, Tuple +from flask_babel import gettext as __ from sqlalchemy.engine.url import URL from superset import security_manager from superset.db_engine_specs.sqlite import SqliteEngineSpec +from superset.errors import SupersetErrorType + +SYNTAX_ERROR_REGEX = re.compile('SQLError: near "(?P.*?)": syntax error') class GSheetsEngineSpec(SqliteEngineSpec): @@ -30,6 +35,17 @@ class GSheetsEngineSpec(SqliteEngineSpec): allows_joins = False allows_subqueries = True + custom_errors: Dict[Pattern[str], Tuple[str, SupersetErrorType, Dict[str, Any]]] = { + SYNTAX_ERROR_REGEX: ( + __( + 'Please check your query for syntax errors near "%(server_error)s". ' + "Then, try running your query again." + ), + SupersetErrorType.SYNTAX_ERROR, + {}, + ), + } + @classmethod def modify_url_for_impersonation( cls, url: URL, impersonate_user: bool, username: Optional[str] diff --git a/superset/db_engine_specs/mysql.py b/superset/db_engine_specs/mysql.py index 4bb5979706b5..01be0c6e13d0 100644 --- a/superset/db_engine_specs/mysql.py +++ b/superset/db_engine_specs/mysql.py @@ -52,6 +52,11 @@ ) CONNECTION_UNKNOWN_DATABASE_REGEX = re.compile("Unknown database '(?P.*?)'") +SYNTAX_ERROR_REGEX = re.compile( + "check the manual that corresponds to your MySQL server " + "version for the right syntax to use near '(?P.*)" +) + class MySQLEngineSpec(BaseEngineSpec, BasicParametersMixin): engine = "mysql" @@ -134,6 +139,14 @@ class MySQLEngineSpec(BaseEngineSpec, BasicParametersMixin): SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR, {"invalid": ["database"]}, ), + SYNTAX_ERROR_REGEX: ( + __( + 'Please check your query for syntax errors near "%(server_error)s". ' + "Then, try running your query again." + ), + SupersetErrorType.SYNTAX_ERROR, + {}, + ), } @classmethod diff --git a/superset/db_engine_specs/postgres.py b/superset/db_engine_specs/postgres.py index 03c955a67d81..ee95d5dd3d50 100644 --- a/superset/db_engine_specs/postgres.py +++ b/superset/db_engine_specs/postgres.py @@ -85,6 +85,8 @@ class FixedOffsetTimezone(_FixedOffset): r"does not exist\s+LINE (?P\d+?)" ) +SYNTAX_ERROR_REGEX = re.compile('syntax error at or near "(?P.*?)"') + class PostgresBaseEngineSpec(BaseEngineSpec): """ Abstract class for Postgres 'like' databases """ @@ -151,6 +153,14 @@ class PostgresBaseEngineSpec(BaseEngineSpec): SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR, {}, ), + SYNTAX_ERROR_REGEX: ( + __( + "Please check your query for syntax errors at or " + 'near "%(syntax_error)s". Then, try running your query again.' + ), + SupersetErrorType.SYNTAX_ERROR, + {}, + ), } @classmethod diff --git a/tests/db_engine_specs/athena_tests.py b/tests/db_engine_specs/athena_tests.py index d928a986daf3..bb53d8ee0347 100644 --- a/tests/db_engine_specs/athena_tests.py +++ b/tests/db_engine_specs/athena_tests.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. from superset.db_engine_specs.athena import AthenaEngineSpec +from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from tests.db_engine_specs.base_tests import TestDbEngineSpec @@ -31,3 +32,26 @@ def test_convert_dttm(self): AthenaEngineSpec.convert_dttm("TIMESTAMP", dttm), "from_iso8601_timestamp('2019-01-02T03:04:05.678900')", ) + + def test_extract_errors(self): + """ + Test that custom error messages are extracted correctly. + """ + msg = ": mismatched input 'fromm'. Expecting: " + result = AthenaEngineSpec.extract_errors(Exception(msg)) + assert result == [ + SupersetError( + message='Please check your query for syntax errors at or near "fromm". Then, try running your query again.', + error_type=SupersetErrorType.SYNTAX_ERROR, + level=ErrorLevel.ERROR, + extra={ + "engine_name": "Amazon Athena", + "issue_codes": [ + { + "code": 1030, + "message": "Issue 1030 - The query has a syntax error.", + } + ], + }, + ) + ] diff --git a/tests/db_engine_specs/gsheets_tests.py b/tests/db_engine_specs/gsheets_tests.py new file mode 100644 index 000000000000..e9a021d8dca9 --- /dev/null +++ b/tests/db_engine_specs/gsheets_tests.py @@ -0,0 +1,44 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from superset.db_engine_specs.gsheets import GSheetsEngineSpec +from superset.errors import ErrorLevel, SupersetError, SupersetErrorType +from tests.db_engine_specs.base_tests import TestDbEngineSpec + + +class TestGsheetsDbEngineSpec(TestDbEngineSpec): + def test_extract_errors(self): + """ + Test that custom error messages are extracted correctly. + """ + msg = 'SQLError: near "fromm": syntax error' + result = GSheetsEngineSpec.extract_errors(Exception(msg)) + assert result == [ + SupersetError( + message='Please check your query for syntax errors near "fromm". Then, try running your query again.', + error_type=SupersetErrorType.SYNTAX_ERROR, + level=ErrorLevel.ERROR, + extra={ + "engine_name": "Google Sheets", + "issue_codes": [ + { + "code": 1030, + "message": "Issue 1030 - The query has a syntax error.", + } + ], + }, + ) + ] diff --git a/tests/db_engine_specs/mysql_tests.py b/tests/db_engine_specs/mysql_tests.py index aa23f2935f58..7f0755ac1308 100644 --- a/tests/db_engine_specs/mysql_tests.py +++ b/tests/db_engine_specs/mysql_tests.py @@ -199,7 +199,6 @@ def test_extract_errors(self): msg = "mysql: Unknown database 'badDB'" result = MySQLEngineSpec.extract_errors(Exception(msg)) - print(result) assert result == [ SupersetError( message='Unable to connect to database "badDB".', @@ -217,3 +216,22 @@ def test_extract_errors(self): }, ) ] + + msg = "check the manual that corresponds to your MySQL server version for the right syntax to use near 'fromm" + result = MySQLEngineSpec.extract_errors(Exception(msg)) + assert result == [ + SupersetError( + message='Please check your query for syntax errors near "fromm". Then, try running your query again.', + error_type=SupersetErrorType.SYNTAX_ERROR, + level=ErrorLevel.ERROR, + extra={ + "engine_name": "MySQL", + "issue_codes": [ + { + "code": 1030, + "message": "Issue 1030 - The query has a syntax error.", + } + ], + }, + ) + ] diff --git a/tests/db_engine_specs/postgres_tests.py b/tests/db_engine_specs/postgres_tests.py index 135621b5e5c9..7f999f07dbd4 100644 --- a/tests/db_engine_specs/postgres_tests.py +++ b/tests/db_engine_specs/postgres_tests.py @@ -421,6 +421,25 @@ def test_extract_errors(self): ) ] + msg = 'syntax error at or near "fromm"' + result = PostgresEngineSpec.extract_errors(Exception(msg)) + assert result == [ + SupersetError( + message='Please check your query for syntax errors at or near "fromm". Then, try running your query again.', + error_type=SupersetErrorType.SYNTAX_ERROR, + level=ErrorLevel.ERROR, + extra={ + "engine_name": "PostgreSQL", + "issue_codes": [ + { + "code": 1030, + "message": "Issue 1030 - The query has a syntax error.", + } + ], + }, + ) + ] + def test_base_parameters_mixin(): parameters = {