diff --git a/docs/src/pages/docs/Miscellaneous/issue_codes.mdx b/docs/src/pages/docs/Miscellaneous/issue_codes.mdx index df8b65f06f2..9abb6253a6e 100644 --- a/docs/src/pages/docs/Miscellaneous/issue_codes.mdx +++ b/docs/src/pages/docs/Miscellaneous/issue_codes.mdx @@ -271,3 +271,21 @@ One or more parameters specified in the query are malformatted. ``` The query contains one or more malformed template parameters. Please check your query and confirm that all template parameters are surround by double braces, for example, "{{ ds }}". Then, try running your query again. +The object does not exist in this database. +``` + +## Issue 1029 + +``` +The object does not exist in this database. +``` + +Either the schema, column, or table do not exist in the database. + +## Issue 1030 + +``` +The query potentially has a syntax error. +``` + +The query might have a syntax error. Please check and run again. diff --git a/superset-frontend/src/components/ErrorMessage/types.ts b/superset-frontend/src/components/ErrorMessage/types.ts index 4f16d385e91..e70d61cf85a 100644 --- a/superset-frontend/src/components/ErrorMessage/types.ts +++ b/superset-frontend/src/components/ErrorMessage/types.ts @@ -39,6 +39,8 @@ export const ErrorTypeEnum = { CONNECTION_DATABASE_PERMISSIONS_ERROR: 'CONNECTION_DATABASE_PERMISSIONS_ERROR', CONNECTION_MISSING_PARAMETERS_ERRORS: 'CONNECTION_MISSING_PARAMETERS_ERRORS', + OBJECT_DOES_NOT_EXIST_ERROR: 'OBJECT_DOES_NOT_EXIST_ERROR', + SYNTAX_ERROR: 'SYNTAX_ERROR', // Viz errors VIZ_GET_DF_ERROR: 'VIZ_GET_DF_ERROR', diff --git a/superset-frontend/src/setup/setupErrorMessages.ts b/superset-frontend/src/setup/setupErrorMessages.ts index 022edeed345..e8d8198245a 100644 --- a/superset-frontend/src/setup/setupErrorMessages.ts +++ b/superset-frontend/src/setup/setupErrorMessages.ts @@ -107,6 +107,14 @@ export default function setupErrorMessages() { ErrorTypeEnum.SCHEMA_DOES_NOT_EXIST_ERROR, DatabaseErrorMessage, ); + errorMessageComponentRegistry.registerValue( + ErrorTypeEnum.OBJECT_DOES_NOT_EXIST_ERROR, + DatabaseErrorMessage, + ); + errorMessageComponentRegistry.registerValue( + ErrorTypeEnum.SYNTAX_ERROR, + DatabaseErrorMessage, + ); errorMessageComponentRegistry.registerValue( ErrorTypeEnum.CONNECTION_DATABASE_PERMISSIONS_ERROR, DatabaseErrorMessage, diff --git a/superset/db_engine_specs/athena.py b/superset/db_engine_specs/athena.py index d82591756a0..2952d826777 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/bigquery.py b/superset/db_engine_specs/bigquery.py index 8951c7e215f..d360b1bbd2c 100644 --- a/superset/db_engine_specs/bigquery.py +++ b/superset/db_engine_specs/bigquery.py @@ -44,6 +44,24 @@ CONNECTION_DATABASE_PERMISSIONS_REGEX = re.compile( + "permission in project (?P.+?)" ) +TABLE_DOES_NOT_EXIST_REGEX = re.compile( + 'Table name "(?P.*?)" missing dataset while no default ' + "dataset is set in the request" +) + +COLUMN_DOES_NOT_EXIST_REGEX = re.compile( + r"Unrecognized name: (?P.*?) at \[(?P.+?)\]" +) + +SCHEMA_DOES_NOT_EXIST_REGEX = re.compile( + r"bigquery error: 404 Not found: Dataset (?P.*?):" + r"(?P.*?) was not found in location" +) + +SYNTAX_ERROR_REGEX = re.compile( + 'Syntax error: Expected end of input but got identifier "(?P.+?)"' +) + ma_plugin = MarshmallowPlugin() @@ -127,6 +145,35 @@ class BigQueryEngineSpec(BaseEngineSpec): SupersetErrorType.CONNECTION_DATABASE_PERMISSIONS_ERROR, {}, ), + TABLE_DOES_NOT_EXIST_REGEX: ( + __( + 'The table "%(table)s" does not exist. ' + "A valid table must be used to run this query.", + ), + SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR, + {}, + ), + COLUMN_DOES_NOT_EXIST_REGEX: ( + __('We can\'t seem to resolve column "%(column)s" at line %(location)s.'), + SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR, + {}, + ), + SCHEMA_DOES_NOT_EXIST_REGEX: ( + __( + 'The schema "%(schema)s" does not exist. ' + "A valid schema must be used to run this query." + ), + SupersetErrorType.SCHEMA_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/superset/db_engine_specs/gsheets.py b/superset/db_engine_specs/gsheets.py index f715cd43b33..1773a3db5cc 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 4bb5979706b..01be0c6e13d 100644 --- a/superset/db_engine_specs/mysql.py +++ b/superset/db_engine_specs/mysql.py @@ -52,6 +52,11 @@ CONNECTION_HOST_DOWN_REGEX = re.compile( ) 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 03c955a67d8..ee95d5dd3d5 100644 --- a/superset/db_engine_specs/postgres.py +++ b/superset/db_engine_specs/postgres.py @@ -85,6 +85,8 @@ COLUMN_DOES_NOT_EXIST_REGEX = re.compile( 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/superset/db_engine_specs/snowflake.py b/superset/db_engine_specs/snowflake.py index 7cce0581e9f..11e1cd414f0 100644 --- a/superset/db_engine_specs/snowflake.py +++ b/superset/db_engine_specs/snowflake.py @@ -15,18 +15,31 @@ # specific language governing permissions and limitations # under the License. import json +import re from datetime import datetime -from typing import Optional, TYPE_CHECKING +from typing import Any, Dict, Optional, Pattern, Tuple, TYPE_CHECKING from urllib import parse +from flask_babel import gettext as __ from sqlalchemy.engine.url import URL from superset.db_engine_specs.postgres import PostgresBaseEngineSpec +from superset.errors import SupersetErrorType from superset.utils import core as utils if TYPE_CHECKING: from superset.models.core import Database +# Regular expressions to catch custom errors +OBJECT_DOES_NOT_EXIST_REGEX = re.compile( + r"Object (?P.*?) does not exist or not authorized." +) + +SYNTAX_ERROR_REGEX = re.compile( + "syntax error line (?P.+?) at position (?P.+?) " + "unexpected '(?P.+?)'." +) + class SnowflakeEngineSpec(PostgresBaseEngineSpec): engine = "snowflake" @@ -54,6 +67,22 @@ class SnowflakeEngineSpec(PostgresBaseEngineSpec): "P1Y": "DATE_TRUNC('YEAR', {col})", } + custom_errors: Dict[Pattern[str], Tuple[str, SupersetErrorType, Dict[str, Any]]] = { + OBJECT_DOES_NOT_EXIST_REGEX: ( + __("%(object)s does not exist in this database."), + SupersetErrorType.OBJECT_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 def adjust_database_uri( cls, uri: URL, selected_schema: Optional[str] = None diff --git a/superset/errors.py b/superset/errors.py index d50030c1ebd..63cd9413077 100644 --- a/superset/errors.py +++ b/superset/errors.py @@ -49,6 +49,8 @@ class SupersetErrorType(str, Enum): CONNECTION_UNKNOWN_DATABASE_ERROR = "CONNECTION_UNKNOWN_DATABASE_ERROR" CONNECTION_DATABASE_PERMISSIONS_ERROR = "CONNECTION_DATABASE_PERMISSIONS_ERROR" CONNECTION_MISSING_PARAMETERS_ERROR = "CONNECTION_MISSING_PARAMETERS_ERROR" + OBJECT_DOES_NOT_EXIST_ERROR = "OBJECT_DOES_NOT_EXIST_ERROR" + SYNTAX_ERROR = "SYNTAX_ERROR" # Viz errors VIZ_GET_DF_ERROR = "VIZ_GET_DF_ERROR" @@ -320,6 +322,17 @@ ERROR_TYPES_TO_ISSUE_CODES_MAPPING = { ), }, ], + SupersetErrorType.OBJECT_DOES_NOT_EXIST_ERROR: [ + { + "code": 1029, + "message": _( + "Issue 1029 - The object does not exist in the given database." + ), + }, + ], + SupersetErrorType.SYNTAX_ERROR: [ + {"code": 1030, "message": _("Issue 1029 - The query has a syntax error."),}, + ], } diff --git a/tests/db_engine_specs/athena_tests.py b/tests/db_engine_specs/athena_tests.py index d928a986daf..bb53d8ee034 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 @@ class TestAthenaDbEngineSpec(TestDbEngineSpec): 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/bigquery_tests.py b/tests/db_engine_specs/bigquery_tests.py index 81a9f064429..d21f3e31877 100644 --- a/tests/db_engine_specs/bigquery_tests.py +++ b/tests/db_engine_specs/bigquery_tests.py @@ -247,3 +247,91 @@ class TestBigQueryDbEngineSpec(TestDbEngineSpec): }, ) ] + + msg = "bigquery error: 404 Not found: Dataset fakeDataset:bogusSchema was not found in location" + result = BigQueryEngineSpec.extract_errors(Exception(msg)) + assert result == [ + SupersetError( + message='The schema "bogusSchema" 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": "Google BigQuery", + "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 = 'Table name "badtable" missing dataset while no default dataset is set in the request' + result = BigQueryEngineSpec.extract_errors(Exception(msg)) + assert result == [ + SupersetError( + message='The table "badtable" 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": "Google BigQuery", + "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 = "Unrecognized name: badColumn at [1:8]" + result = BigQueryEngineSpec.extract_errors(Exception(msg)) + assert result == [ + SupersetError( + message='We can\'t seem to resolve column "badColumn" at line 1:8.', + error_type=SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR, + level=ErrorLevel.ERROR, + extra={ + "engine_name": "Google BigQuery", + "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 = 'Syntax error: Expected end of input but got identifier "fromm"' + result = BigQueryEngineSpec.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": "Google BigQuery", + "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 00000000000..e9a021d8dca --- /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 aa23f2935f5..7f0755ac130 100644 --- a/tests/db_engine_specs/mysql_tests.py +++ b/tests/db_engine_specs/mysql_tests.py @@ -199,7 +199,6 @@ class TestMySQLEngineSpecsDbEngineSpec(TestDbEngineSpec): 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 @@ class TestMySQLEngineSpecsDbEngineSpec(TestDbEngineSpec): }, ) ] + + 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 135621b5e5c..7f999f07dbd 100644 --- a/tests/db_engine_specs/postgres_tests.py +++ b/tests/db_engine_specs/postgres_tests.py @@ -421,6 +421,25 @@ psql: error: could not connect to server: Operation timed out ) ] + 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 = { diff --git a/tests/db_engine_specs/snowflake_tests.py b/tests/db_engine_specs/snowflake_tests.py index e8f76fe7b6f..14ca728df1a 100644 --- a/tests/db_engine_specs/snowflake_tests.py +++ b/tests/db_engine_specs/snowflake_tests.py @@ -17,6 +17,7 @@ import json from superset.db_engine_specs.snowflake import SnowflakeEngineSpec +from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.models.core import Database from tests.db_engine_specs.base_tests import TestDbEngineSpec @@ -43,3 +44,42 @@ class TestSnowflakeDbEngineSpec(TestDbEngineSpec): {"engine_params": {"connect_args": {"validate_default_parameters": True}}}, engine_params, ) + + def test_extract_errors(self): + msg = "Object dumbBrick does not exist or not authorized." + result = SnowflakeEngineSpec.extract_errors(Exception(msg)) + assert result == [ + SupersetError( + message="dumbBrick does not exist in this database.", + error_type=SupersetErrorType.OBJECT_DOES_NOT_EXIST_ERROR, + level=ErrorLevel.ERROR, + extra={ + "engine_name": "Snowflake", + "issue_codes": [ + { + "code": 1029, + "message": "Issue 1029 - The object does not exist in the given database.", + } + ], + }, + ) + ] + + msg = "syntax error line 1 at position 10 unexpected 'limmmited'." + result = SnowflakeEngineSpec.extract_errors(Exception(msg)) + assert result == [ + SupersetError( + message='Please check your query for syntax errors at or near "limmmited". Then, try running your query again.', + error_type=SupersetErrorType.SYNTAX_ERROR, + level=ErrorLevel.ERROR, + extra={ + "engine_name": "Snowflake", + "issue_codes": [ + { + "code": 1030, + "message": "Issue 1030 - The query has a syntax error.", + } + ], + }, + ) + ]