feat: show rich error messages on past failed queries (#15158)

* feat: store SIP-40 error payload with queries

* Set errors in query on load
This commit is contained in:
Beto Dealmeida 2021-06-16 12:58:14 -07:00 committed by GitHub
parent e689b0d445
commit d625f5f111
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 39 additions and 11 deletions

View File

@ -118,6 +118,9 @@ export default function SouthPane({
} }
let results; let results;
if (latestQuery) { if (latestQuery) {
if (latestQuery?.extra?.errors) {
latestQuery.errors = latestQuery.extra.errors;
}
if ( if (
isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) && isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) &&
latestQuery.state === 'success' && latestQuery.state === 'success' &&

View File

@ -14,12 +14,15 @@
# KIND, either express or implied. See the License for the # KIND, either express or implied. See the License for the
# specific language governing permissions and limitations # specific language governing permissions and limitations
# under the License. # under the License.
import re
from datetime import datetime from datetime import datetime
from typing import List, Optional, TYPE_CHECKING from typing import Any, Dict, List, Optional, Pattern, Tuple, TYPE_CHECKING
from flask_babel import gettext as __
from sqlalchemy.engine.reflection import Inspector from sqlalchemy.engine.reflection import Inspector
from superset.db_engine_specs.base import BaseEngineSpec from superset.db_engine_specs.base import BaseEngineSpec
from superset.errors import SupersetErrorType
from superset.utils import core as utils from superset.utils import core as utils
if TYPE_CHECKING: if TYPE_CHECKING:
@ -27,6 +30,9 @@ if TYPE_CHECKING:
from superset.models.core import Database from superset.models.core import Database
COLUMN_DOES_NOT_EXIST_REGEX = re.compile("no such column: (?P<column_name>.+)")
class SqliteEngineSpec(BaseEngineSpec): class SqliteEngineSpec(BaseEngineSpec):
engine = "sqlite" engine = "sqlite"
engine_name = "SQLite" engine_name = "SQLite"
@ -53,6 +59,14 @@ class SqliteEngineSpec(BaseEngineSpec):
"1969-12-28T00:00:00Z/P1W": "DATE({col}, 'weekday 0', '-7 days')", "1969-12-28T00:00:00Z/P1W": "DATE({col}, 'weekday 0', '-7 days')",
} }
custom_errors: Dict[Pattern[str], Tuple[str, SupersetErrorType, Dict[str, Any]]] = {
COLUMN_DOES_NOT_EXIST_REGEX: (
__('We can\'t seem to resolve the column "%(column_name)s"'),
SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR,
{},
),
}
@classmethod @classmethod
def epoch_to_dttm(cls) -> str: def epoch_to_dttm(cls) -> str:
return "datetime({col}, 'unixepoch')" return "datetime({col}, 'unixepoch')"

View File

@ -14,6 +14,7 @@
# KIND, either express or implied. See the License for the # KIND, either express or implied. See the License for the
# specific language governing permissions and limitations # specific language governing permissions and limitations
# under the License. # under the License.
import dataclasses
import logging import logging
import uuid import uuid
from contextlib import closing from contextlib import closing
@ -93,8 +94,17 @@ def handle_query_error(
query.error_message = msg query.error_message = msg
query.status = QueryStatus.FAILED query.status = QueryStatus.FAILED
query.tmp_table_name = None query.tmp_table_name = None
# extract DB-specific errors (invalid column, eg)
errors = [
dataclasses.asdict(error)
for error in query.database.db_engine_spec.extract_errors(msg)
]
if errors:
query.set_extra_json_key("errors", errors)
session.commit() session.commit()
payload.update({"status": query.status, "error": msg}) payload.update({"status": query.status, "error": msg, "errors": errors})
if troubleshooting_link: if troubleshooting_link:
payload["link"] = troubleshooting_link payload["link"] = troubleshooting_link
return payload return payload

View File

@ -73,6 +73,7 @@ from superset.dashboards.dao import DashboardDAO
from superset.databases.dao import DatabaseDAO from superset.databases.dao import DatabaseDAO
from superset.databases.filters import DatabaseFilter from superset.databases.filters import DatabaseFilter
from superset.datasets.commands.exceptions import DatasetNotFoundError from superset.datasets.commands.exceptions import DatasetNotFoundError
from superset.errors import SupersetError
from superset.exceptions import ( from superset.exceptions import (
CacheLoadError, CacheLoadError,
CertificateException, CertificateException,
@ -2427,15 +2428,15 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
raise SupersetGenericDBErrorException(utils.error_msg_from_exception(ex)) raise SupersetGenericDBErrorException(utils.error_msg_from_exception(ex))
if data.get("status") == QueryStatus.FAILED: if data.get("status") == QueryStatus.FAILED:
msg = data["error"] # new error payload with rich context
query = _session.query(Query).filter_by(id=query_id).one() if data["errors"]:
database = query.database raise SupersetErrorsException(
db_engine_spec = database.db_engine_spec [SupersetError(**params) for params in data["errors"]]
errors = db_engine_spec.extract_errors(msg) )
_session.close()
if errors: # old string-only error message
raise SupersetErrorsException(errors) raise SupersetGenericDBErrorException(data["error"])
raise SupersetGenericDBErrorException(msg)
return json_success(payload) return json_success(payload)
@has_access_api @has_access_api