Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show missing parameters in query #12049

Merged
merged 5 commits into from
Dec 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/src/pages/docs/Miscellaneous/issue_codes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,14 @@ The table was deleted or renamed in the database.
Your query failed because it is referencing a table that no longer exists in
the underlying database. You should modify your query to reference the correct
table.

## Issue 1006

```
One or more parameters specified in the query are missing.
```

Your query was not submitted to the database because it's missing one or more
parameters. You should define all the parameters referenced in the query in a
valid JSON document. Check that the parameters are spelled correctly and that
the document has a valid syntax.
10 changes: 9 additions & 1 deletion superset-frontend/src/SqlLab/components/ResultSet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { styled, t } from '@superset-ui/core';
import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace';
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
import { getByUser, put as updateDatset } from 'src/api/dataset';
import { ErrorTypeEnum } from 'src/components/ErrorMessage/types';
import Loading from '../../components/Loading';
import ExploreCtasResultsButton from './ExploreCtasResultsButton';
import ExploreResultsButton from './ExploreResultsButton';
Expand Down Expand Up @@ -489,10 +490,17 @@ export default class ResultSet extends React.PureComponent<
return <Alert bsStyle="warning">Query was stopped</Alert>;
}
if (query.state === 'failed') {
// TODO (betodealmeida): handle this logic through the error component
// registry
const title =
query?.errors?.[0].error_type ===
ErrorTypeEnum.MISSING_TEMPLATE_PARAMS_ERROR
? t('Parameter Error')
: t('Database Error');
betodealmeida marked this conversation as resolved.
Show resolved Hide resolved
return (
<div className="result-set-error-message">
<ErrorMessageWithStackTrace
title={t('Database Error')}
title={title}
error={query?.errors?.[0]}
subtitle={<MonospaceDiv>{query.errorMessage}</MonospaceDiv>}
copyText={query.errorMessage || undefined}
Expand Down
16 changes: 9 additions & 7 deletions superset-frontend/src/SqlLab/components/SqlEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -566,13 +566,15 @@ class SqlEditor extends React.PureComponent {
<Checkbox checked={this.state.autocompleteEnabled} />{' '}
{t('Autocomplete')}
</Button>{' '}
<TemplateParamsEditor
language="json"
onChange={params => {
this.props.actions.queryEditorSetTemplateParams(qe, params);
}}
code={qe.templateParams}
/>
{isFeatureEnabled(FeatureFlag.ENABLE_TEMPLATE_PROCESSING) && (
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are showing "Parameters" in SQL Lab even though the functionality is now behind a feature flag in the backend. I turned it off here if the functionality is not enabled.

cc: @hughhhh

<TemplateParamsEditor
language="json"
onChange={params => {
this.props.actions.queryEditorSetTemplateParams(qe, params);
}}
code={qe.templateParams}
/>
)}
{limitWarning}
{this.props.latestQuery && (
<Timer
Expand Down
3 changes: 3 additions & 0 deletions superset-frontend/src/components/ErrorMessage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export const ErrorTypeEnum = {

// Other errors
BACKEND_TIMEOUT_ERROR: 'BACKEND_TIMEOUT_ERROR',

// Sqllab error
MISSING_TEMPLATE_PARAMS_ERROR: 'MISSING_TEMPLATE_PARAMS_ERROR',
} as const;

type ValueOf<T> = T[keyof T];
Expand Down
1 change: 1 addition & 0 deletions superset-frontend/src/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export enum FeatureFlag {
ESCAPE_MARKDOWN_HTML = 'ESCAPE_MARKDOWN_HTML',
VERSIONED_EXPORT = 'VERSIONED_EXPORT',
GLOBAL_ASYNC_QUERIES = 'GLOBAL_ASYNC_QUERIES',
ENABLE_TEMPLATE_PROCESSING = 'ENABLE_TEMPLATE_PROCESSING',
}

export type FeatureFlagMap = {
Expand Down
13 changes: 13 additions & 0 deletions superset/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class SupersetErrorType(str, Enum):
Types of errors that can exist within Superset.

Keep in sync with superset-frontend/src/components/ErrorMessage/types.ts
and docs/src/pages/docs/Miscellaneous/issue_codes.mdx
"""

# Frontend errors
Expand All @@ -52,6 +53,9 @@ class SupersetErrorType(str, Enum):
# Other errors
BACKEND_TIMEOUT_ERROR = "BACKEND_TIMEOUT_ERROR"

# Sql Lab errors
MISSING_TEMPLATE_PARAMS_ERROR = "MISSING_TEMPLATE_PARAMS_ERROR"


ERROR_TYPES_TO_ISSUE_CODES_MAPPING = {
SupersetErrorType.BACKEND_TIMEOUT_ERROR: [
Expand Down Expand Up @@ -100,6 +104,15 @@ class SupersetErrorType(str, Enum):
),
},
],
SupersetErrorType.MISSING_TEMPLATE_PARAMS_ERROR: [
betodealmeida marked this conversation as resolved.
Show resolved Hide resolved
{
"code": 1006,
"message": _(
"Issue 1006 - One or more parameters specified in the query are "
"missing."
),
},
],
}


Expand Down
5 changes: 3 additions & 2 deletions superset/jinja_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from flask import current_app, g, request
from flask_babel import gettext as _
from jinja2 import DebugUndefined
from jinja2.sandbox import SandboxedEnvironment

from superset.exceptions import SupersetTemplateException
Expand Down Expand Up @@ -60,7 +61,7 @@ def context_addons() -> Dict[str, Any]:


def filter_values(column: str, default: Optional[str] = None) -> List[str]:
""" Gets a values for a particular filter as a list
"""Gets a values for a particular filter as a list

This is useful if:
- you want to use a filter box to filter a query where the name of filter box
Expand Down Expand Up @@ -296,7 +297,7 @@ def __init__(
self._schema = table.schema
self._extra_cache_keys = extra_cache_keys
self._context: Dict[str, Any] = {}
self._env = SandboxedEnvironment()
self._env = SandboxedEnvironment(undefined=DebugUndefined)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is DebugUndefined required to make find_undeclared_variables function? Doesn't seem to be a requirement in the docs.

self.set_context(**kwargs)

def set_context(self, **kwargs: Any) -> None:
Expand Down
5 changes: 5 additions & 0 deletions superset/utils/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1669,3 +1669,8 @@ def get_time_filter_status( # pylint: disable=too-many-branches
)

return applied, rejected


def format_list(items: Sequence[str], sep: str = ", ", quote: str = '"') -> str:
quote_escaped = "\\" + quote
return sep.join(f"{quote}{x.replace(quote, quote_escaped)}{quote}" for x in items)
34 changes: 32 additions & 2 deletions superset/views/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=comparison-with-callable
# pylint: disable=comparison-with-callable, line-too-long, too-many-branches
import dataclasses
import logging
import re
from contextlib import closing
Expand All @@ -34,8 +35,9 @@
permission_name,
)
from flask_appbuilder.security.sqla import models as ab_models
from flask_babel import gettext as __, lazy_gettext as _
from flask_babel import gettext as __, lazy_gettext as _, ngettext
from jinja2.exceptions import TemplateError
from jinja2.meta import find_undeclared_variables
from sqlalchemy import and_, or_
from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import ArgumentError, DBAPIError, NoSuchModuleError, SQLAlchemyError
Expand Down Expand Up @@ -69,6 +71,7 @@
from superset.dashboards.dao import DashboardDAO
from superset.databases.dao import DatabaseDAO
from superset.databases.filters import DatabaseFilter
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import (
CacheLoadError,
CertificateException,
Expand Down Expand Up @@ -157,6 +160,11 @@

DATASOURCE_MISSING_ERR = __("The data source seems to have been deleted")
USER_MISSING_ERR = __("The user seems to have been deleted")
PARAMETER_MISSING_ERR = (
"Please check your template parameters for syntax errors and make sure "
"they match across your SQL query and Set Parameters. Then, try running "
"your query again."
)


class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
Expand Down Expand Up @@ -2506,6 +2514,28 @@ def sql_json_exec( # pylint: disable=too-many-statements,too-many-locals
f"Query {query_id}: Template syntax error: {error_msg}"
)

# pylint: disable=protected-access
if is_feature_enabled("ENABLE_TEMPLATE_PROCESSING"):
ast = template_processor._env.parse(rendered_query)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: accessing a private property from outside the class

undefined = find_undeclared_variables(ast) # type: ignore
Comment on lines +2519 to +2520
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robdiciuccio want to confirm that this is ok?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not guaranteed that the template processor with be Jinja-based, due to how CUSTOM_TEMPLATE_PROCESSORS operates. I suggest checking that template_processor is an instance of JinjaTemplateProcessor here instead of checking ENABLE_TEMPLATE_PROCESSING.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than that, I don't see any security issues with using find_undeclared_variables in this context. I'm assuming that undefined variables don't raise an error during process_template?

if undefined:
error = SupersetError(
message=ngettext(
"There's an error with the parameter %(parameters)s.",
"There's an error with the parameters %(parameters)s.",
len(undefined),
parameters=utils.format_list(undefined),
)
+ " "
+ PARAMETER_MISSING_ERR,
level=ErrorLevel.ERROR,
error_type=SupersetErrorType.MISSING_TEMPLATE_PARAMS_ERROR,
extra={"missing_parameters": list(undefined)},
)
return json_error_response(
payload={"errors": [dataclasses.asdict(error)]},
)

# Limit is not applied to the CTA queries if SQLLAB_CTAS_NO_LIMIT flag is set
# to True.
if not (config.get("SQLLAB_CTAS_NO_LIMIT") and select_as_cta):
Expand Down
2 changes: 2 additions & 0 deletions tests/base_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ def run_sql(
tmp_table_name=None,
schema=None,
ctas_method=CtasMethod.TABLE,
template_params="{}",
):
if user_name:
self.logout()
Expand All @@ -336,6 +337,7 @@ def run_sql(
"queryLimit": query_limit,
"sql_editor_id": sql_editor_id,
"ctas_method": ctas_method,
"templateParams": template_params,
}
if tmp_table_name:
json_payload["tmp_table_name"] = tmp_table_name
Expand Down
21 changes: 20 additions & 1 deletion tests/sqllab_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,6 @@ def test_query_admin_can_access_all_queries(self) -> None:

url = "/api/v1/query/"
data = self.get_json_resp(url)
admin = security_manager.find_user("admin")
self.assertEqual(3, len(data["result"]))

def test_api_database(self):
Expand All @@ -576,3 +575,23 @@ def test_api_database(self):
{r.get("database_name") for r in self.get_json_resp(url)["result"]},
)
self.delete_fake_db()

@mock.patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
{"ENABLE_TEMPLATE_PROCESSING": True},
clear=True,
)
def test_sql_json_parameter_error(self):
self.login("admin")

data = self.run_sql(
"SELECT * FROM birth_names WHERE state = '{{ state }}' LIMIT 10",
"1",
template_params=json.dumps({"state": "CA"}),
)
assert data["status"] == "success"

data = self.run_sql(
"SELECT * FROM birth_names WHERE state = '{{ state }}' LIMIT 10", "2"
)
assert data["errors"][0]["error_type"] == "MISSING_TEMPLATE_PARAMS_ERROR"