airbnb/caravel

View on GitHub
superset/sqllab/query_render.py

Summary

Maintainability
A
45 mins
Test Coverage
# 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.
# pylint: disable=invalid-name, too-few-public-methods, too-many-arguments
from __future__ import annotations

from typing import Any, Callable, TYPE_CHECKING

from flask_babel import gettext as __, ngettext
from jinja2 import TemplateError
from jinja2.meta import find_undeclared_variables

from superset import is_feature_enabled
from superset.commands.sql_lab.execute import SqlQueryRender
from superset.errors import SupersetErrorType
from superset.sql_parse import ParsedQuery
from superset.sqllab.exceptions import SqlLabException
from superset.utils import core as utils

MSG_OF_1006 = "Issue 1006 - One or more parameters specified in the query are missing."

if TYPE_CHECKING:
    from superset.jinja_context import BaseTemplateProcessor
    from superset.sqllab.sqllab_execution_context import SqlJsonExecutionContext

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 SqlQueryRenderImpl(SqlQueryRender):
    _sql_template_processor_factory: Callable[..., BaseTemplateProcessor]

    def __init__(
        self, sql_template_factory: Callable[..., BaseTemplateProcessor]
    ) -> None:
        self._sql_template_processor_factory = sql_template_factory

    def render(self, execution_context: SqlJsonExecutionContext) -> str:
        query_model = execution_context.query
        try:
            sql_template_processor = self._sql_template_processor_factory(
                database=query_model.database, query=query_model
            )

            parsed_query = ParsedQuery(
                query_model.sql,
                engine=query_model.database.db_engine_spec.engine,
            )
            rendered_query = sql_template_processor.process_template(
                parsed_query.stripped(), **execution_context.template_params
            )
            self._validate(execution_context, rendered_query, sql_template_processor)
            return rendered_query
        except TemplateError as ex:
            self._raise_template_exception(ex, execution_context)
            return "NOT_REACHABLE_CODE"

    def _validate(
        self,
        execution_context: SqlJsonExecutionContext,
        rendered_query: str,
        sql_template_processor: BaseTemplateProcessor,
    ) -> None:
        if is_feature_enabled("ENABLE_TEMPLATE_PROCESSING"):
            syntax_tree = sql_template_processor.env.parse(rendered_query)
            undefined_parameters = find_undeclared_variables(syntax_tree)
            if undefined_parameters:
                self._raise_undefined_parameter_exception(
                    execution_context, undefined_parameters
                )

    def _raise_undefined_parameter_exception(
        self, execution_context: SqlJsonExecutionContext, undefined_parameters: Any
    ) -> None:
        raise SqlQueryRenderException(
            sql_json_execution_context=execution_context,
            error_type=SupersetErrorType.MISSING_TEMPLATE_PARAMS_ERROR,
            reason_message=ngettext(
                "The parameter %(parameters)s in your query is undefined.",
                "The following parameters in your query are undefined: %(parameters)s.",
                len(undefined_parameters),
                parameters=utils.format_list(undefined_parameters),
            ),
            suggestion_help_msg=PARAMETER_MISSING_ERR,
            extra={
                "undefined_parameters": list(undefined_parameters),
                "template_parameters": execution_context.template_params,
                "issue_codes": [
                    {
                        "code": 1006,
                        "message": MSG_OF_1006,
                    }
                ],
            },
        )

    def _raise_template_exception(
        self, ex: Exception, execution_context: SqlJsonExecutionContext
    ) -> None:
        raise SqlQueryRenderException(
            sql_json_execution_context=execution_context,
            error_type=SupersetErrorType.INVALID_TEMPLATE_PARAMS_ERROR,
            reason_message=__(
                "The query contains one or more malformed template parameters."
            ),
            suggestion_help_msg=__(
                "Please check your query and confirm that all template "
                "parameters are surround by double braces, for example, "
                '"{{ ds }}". Then, try running your query again.'
            ),
        ) from ex


class SqlQueryRenderException(SqlLabException):
    _extra: dict[str, Any] | None

    def __init__(
        self,
        sql_json_execution_context: SqlJsonExecutionContext,
        error_type: SupersetErrorType,
        reason_message: str | None = None,
        exception: Exception | None = None,
        suggestion_help_msg: str | None = None,
        extra: dict[str, Any] | None = None,
    ) -> None:
        super().__init__(
            sql_json_execution_context,
            error_type,
            reason_message,
            exception,
            suggestion_help_msg,
        )
        self._extra = extra

    @property
    def extra(self) -> dict[str, Any] | None:
        return self._extra

    def to_dict(self) -> dict[str, Any]:
        rv = super().to_dict()
        if self._extra:
            rv["extra"] = self._extra
        return rv