Skip to content

Commit

Permalink
feat: Better Errors in SQL Lab (#15432)
Browse files Browse the repository at this point in the history
* snowflake errors

* added big query

* added to setup error messages, first test

* all big query testing added

* added snowflake test

* added syntax error

* added syntax errors to most used databases
  • Loading branch information
AAfghahi committed Jun 29, 2021
1 parent 4a394cd commit 743d9cc
Show file tree
Hide file tree
Showing 16 changed files with 412 additions and 4 deletions.
18 changes: 18 additions & 0 deletions docs/src/pages/docs/Miscellaneous/issue_codes.mdx
Expand Up @@ -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.
2 changes: 2 additions & 0 deletions superset-frontend/src/components/ErrorMessage/types.ts
Expand Up @@ -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',
Expand Down
8 changes: 8 additions & 0 deletions superset-frontend/src/setup/setupErrorMessages.ts
Expand Up @@ -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,
Expand Down
21 changes: 20 additions & 1 deletion superset/db_engine_specs/athena.py
Expand Up @@ -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<syntax_error>.*?)'. Expecting: "
)


class AthenaEngineSpec(BaseEngineSpec):
engine = "awsathena"
Expand All @@ -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()
Expand Down
47 changes: 47 additions & 0 deletions superset/db_engine_specs/bigquery.py
Expand Up @@ -44,6 +44,24 @@
+ "permission in project (?P<project>.+?)"
)

TABLE_DOES_NOT_EXIST_REGEX = re.compile(
'Table name "(?P<table>.*?)" missing dataset while no default '
"dataset is set in the request"
)

COLUMN_DOES_NOT_EXIST_REGEX = re.compile(
r"Unrecognized name: (?P<column>.*?) at \[(?P<location>.+?)\]"
)

SCHEMA_DOES_NOT_EXIST_REGEX = re.compile(
r"bigquery error: 404 Not found: Dataset (?P<dataset>.*?):"
r"(?P<schema>.*?) was not found in location"
)

SYNTAX_ERROR_REGEX = re.compile(
'Syntax error: Expected end of input but got identifier "(?P<syntax_error>.+?)"'
)

ma_plugin = MarshmallowPlugin()


Expand Down Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion superset/db_engine_specs/gsheets.py
Expand Up @@ -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<server_error>.*?)": syntax error')


class GSheetsEngineSpec(SqliteEngineSpec):
Expand All @@ -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]
Expand Down
13 changes: 13 additions & 0 deletions superset/db_engine_specs/mysql.py
Expand Up @@ -52,6 +52,11 @@
)
CONNECTION_UNKNOWN_DATABASE_REGEX = re.compile("Unknown database '(?P<database>.*?)'")

SYNTAX_ERROR_REGEX = re.compile(
"check the manual that corresponds to your MySQL server "
"version for the right syntax to use near '(?P<server_error>.*)"
)


class MySQLEngineSpec(BaseEngineSpec, BasicParametersMixin):
engine = "mysql"
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions superset/db_engine_specs/postgres.py
Expand Up @@ -85,6 +85,8 @@ class FixedOffsetTimezone(_FixedOffset):
r"does not exist\s+LINE (?P<location>\d+?)"
)

SYNTAX_ERROR_REGEX = re.compile('syntax error at or near "(?P<syntax_error>.*?)"')


class PostgresBaseEngineSpec(BaseEngineSpec):
""" Abstract class for Postgres 'like' databases """
Expand Down Expand Up @@ -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
Expand Down
31 changes: 30 additions & 1 deletion superset/db_engine_specs/snowflake.py
Expand Up @@ -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<object>.*?) does not exist or not authorized."
)

SYNTAX_ERROR_REGEX = re.compile(
"syntax error line (?P<line>.+?) at position (?P<position>.+?) "
"unexpected '(?P<syntax_error>.+?)'."
)


class SnowflakeEngineSpec(PostgresBaseEngineSpec):
engine = "snowflake"
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions superset/errors.py
Expand Up @@ -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"
Expand Down Expand Up @@ -320,6 +322,17 @@ class SupersetErrorType(str, Enum):
),
},
],
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."),},
],
}


Expand Down
24 changes: 24 additions & 0 deletions tests/db_engine_specs/athena_tests.py
Expand Up @@ -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


Expand All @@ -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.",
}
],
},
)
]

0 comments on commit 743d9cc

Please sign in to comment.