Repository: apache/superset
File: superset/commands/database/validate_sql.py (Python)

Code before:
    def __init__(self, model_id: int, data: dict[str, Any]):
        self._properties = data.copy()
        self._model_id = model_id
        self._model: Optional[Database] = None
        self._validator: Optional[type[BaseSQLValidator]] = None

    def run(self) -> list[dict[str, Any]]:
        """
        Validates a SQL statement

        :return: A List of SQLValidationAnnotation
        :raises: DatabaseNotFoundError, NoValidatorConfigFoundError
          NoValidatorFoundError, ValidatorSQLUnexpectedError, ValidatorSQLError
          ValidatorSQL400Error
        """
        self.validate()
        if not self._validator or not self._model:
            raise ValidatorSQLUnexpectedError()
        sql = self._properties["sql"]
        catalog = self._properties.get("catalog")
        schema = self._properties.get("schema")
        template_params = self._properties.get("template_params") or {}

        # Render Jinja templates to handle template syntax before validation
        template_processor = get_template_processor(self._model)
        sql = template_processor.process_template(sql, **template_params)

        try:
            timeout = app.config["SQLLAB_VALIDATION_TIMEOUT"]
            timeout_msg = f"The query exceeded the {timeout} seconds timeout."
            with utils.timeout(seconds=timeout, error_message=timeout_msg):
                errors = self._validator.validate(sql, catalog, schema, self._model)
            return [err.to_dict() for err in errors]
        except Exception as ex:
            logger.exception(ex)
            superset_error = SupersetError(
                message=__(
                    "%(validator)s was unable to check your query.\n"
                    "Please recheck your query.\n"
                    "Exception: %(ex)s",
                    validator=self._validator.name,
                    ex=ex,
                ),
                error_type=SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
                level=ErrorLevel.ERROR,
            )

            # Return as a 400 if the database error message says we got a 4xx error
            if re.search(r"([\W]|^)4\d{2}([\W]|$)", str(ex)):
                raise ValidatorSQL400Error(superset_error) from ex
            raise ValidatorSQLError(superset_error) from ex

Review comment (bug):
Could you add a brief comment or a tiny docstring explaining expected semantics of process_template here (e.g., whether it always returns a string, whether it may mutate the SQL, and what exceptions it may raise)? Also, please consider defensive handling/logging if process_template fails (so the validation endpoint can return a meaningful error instead of a 500).

Code after:
logger = logging.getLogger(__name__)


class ValidateSQLCommand(BaseCommand):
    def __init__(self, model_id: int, data: dict[str, Any]):
        self._properties = data.copy()
        self._model_id = model_id
        self._model: Optional[Database] = None
        self._validator: Optional[type[BaseSQLValidator]] = None

    def run(self) -> list[dict[str, Any]]:
        """
        Validates a SQL statement

        :return: A List of SQLValidationAnnotation
        :raises: DatabaseNotFoundError, NoValidatorConfigFoundError
          NoValidatorFoundError, ValidatorSQLUnexpectedError, ValidatorSQLError
          ValidatorSQL400Error
        """
        self.validate()
        if not self._validator or not self._model:
            raise ValidatorSQLUnexpectedError()
        sql = self._properties["sql"]
        catalog = self._properties.get("catalog")
        schema = self._properties.get("schema")
        template_params = self._properties.get("template_params") or {}

        try:
            # Render Jinja templates to handle template syntax before
            # validation. Note: The ENABLE_TEMPLATE_PROCESSING feature flag is
            # checked within get_template_processor(), which returns
            # NoOpTemplateProcessor when disabled. Template processing errors
            # (e.g., undefined filters, syntax errors) are caught by this
            # exception handler and surfaced to the client as
            # ValidatorSQLError or ValidatorSQL400Error with appropriate error
            # messages.
            template_processor = get_template_processor(self._model)
            # process_template() renders Jinja2 templates and always returns a
            # new string (does not mutate the input SQL). May raise
            # SupersetSyntaxErrorException for template syntax errors or
            # SupersetTemplateException for internal errors.
            sql = template_processor.process_template(sql, **template_params)

            timeout = app.config["SQLLAB_VALIDATION_TIMEOUT"]
            timeout_msg = f"The query exceeded the {timeout} seconds timeout."
            with utils.timeout(seconds=timeout, error_message=timeout_msg):
                errors = self._validator.validate(sql, catalog, schema, self._model)
            return [err.to_dict() for err in errors]
        except SupersetSyntaxErrorException as ex:
            # Template syntax errors (e.g., invalid Jinja2 syntax, undefined variables)
            # These contain detailed error information including line numbers