This is an automated email from the ASF dual-hosted git repository.

alexandrusoare pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 6e8b3bf9761 fix(mcp): raise right error (#39964)
6e8b3bf9761 is described below

commit 6e8b3bf9761eea0c7ac7a75d32f8fc6145b53ffd
Author: Alexandru Soare <[email protected]>
AuthorDate: Wed May 20 14:32:45 2026 +0300

    fix(mcp): raise right error (#39964)
---
 superset/mcp_service/chart/compile.py              | 101 +++++++++++++++++++++
 superset/mcp_service/chart/tool/generate_chart.py  |   4 +-
 tests/unit_tests/mcp_service/chart/test_compile.py |  87 ++++++++++++++++++
 3 files changed, 190 insertions(+), 2 deletions(-)

diff --git a/superset/mcp_service/chart/compile.py 
b/superset/mcp_service/chart/compile.py
index 6809e7534a5..62abf3e5f15 100644
--- a/superset/mcp_service/chart/compile.py
+++ b/superset/mcp_service/chart/compile.py
@@ -36,7 +36,10 @@ import logging
 from dataclasses import dataclass, field
 from typing import Any, Dict, List, Literal
 
+from sqlalchemy.exc import SQLAlchemyError
+
 from superset.commands.exceptions import CommandException
+from superset.errors import SupersetErrorType
 from superset.mcp_service.chart.validation.dataset_validator import 
DatasetValidator
 from superset.mcp_service.common.error_schemas import (
     ChartGenerationError,
@@ -46,6 +49,31 @@ from superset.mcp_service.common.error_schemas import (
 
 logger = logging.getLogger(__name__)
 
+# Error types from db_engine_spec.extract_errors() that indicate a database
+# connectivity or authentication issue rather than a query/config problem.
+#
+# GENERIC_DB_ENGINE_ERROR is included because many engines (BigQuery,
+# Snowflake, Athena, Databricks, Trino) lack specific CONNECTION_* regex
+# patterns in their engine specs — all their connection failures fall back
+# to this generic type. This is safe here because _compile_chart only runs
+# after Tier 1 schema validation has already verified columns, metrics, and
+# filters against the dataset. At that point the SQL is auto-generated by
+# Superset's query builder, so genuine SQL/config errors are very unlikely.
+_CONNECTION_ERROR_TYPES = {
+    SupersetErrorType.CONNECTION_INVALID_USERNAME_ERROR,
+    SupersetErrorType.CONNECTION_INVALID_PASSWORD_ERROR,
+    SupersetErrorType.CONNECTION_INVALID_HOSTNAME_ERROR,
+    SupersetErrorType.CONNECTION_PORT_CLOSED_ERROR,
+    SupersetErrorType.CONNECTION_INVALID_PORT_ERROR,
+    SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
+    SupersetErrorType.CONNECTION_ACCESS_DENIED_ERROR,
+    SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR,
+    SupersetErrorType.CONNECTION_DATABASE_PERMISSIONS_ERROR,
+    SupersetErrorType.CONNECTION_MISSING_PARAMETERS_ERROR,
+    SupersetErrorType.CONNECTION_DATABASE_TIMEOUT,
+    SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
+}
+
 
 @dataclass
 class CompileResult:
@@ -186,6 +214,19 @@ def _compile_chart(
 
         return CompileResult(success=True, warnings=warnings, 
row_count=row_count)
     except (ChartDataQueryFailedError, ChartDataCacheLoadError) as exc:
+        if _classify_as_database_error(exc, dataset_id):
+            logger.warning(
+                "Database connection error during chart compile check: %s: %s",
+                type(exc).__name__,
+                str(exc),
+            )
+            return CompileResult(
+                success=False,
+                error=f"Database connection error: {exc}",
+                error_code="CHART_COMPILE_FAILED",
+                tier="compile",
+                error_obj=_build_database_error(str(exc)),
+            )
         return CompileResult(
             success=False,
             error=str(exc),
@@ -201,6 +242,19 @@ def _compile_chart(
             tier="compile",
             error_obj=_build_compile_error(str(exc)),
         )
+    except SQLAlchemyError as exc:
+        logger.warning(
+            "Database connection error during chart compile check: %s: %s",
+            type(exc).__name__,
+            str(exc),
+        )
+        return CompileResult(
+            success=False,
+            error=f"Database connection error: {exc}",
+            error_code="CHART_COMPILE_FAILED",
+            tier="compile",
+            error_obj=_build_database_error(str(exc)),
+        )
 
 
 def _adhoc_filter_column_valid(
@@ -278,6 +332,53 @@ def _validate_adhoc_filter_columns(
     )
 
 
+def _classify_as_database_error(exc: BaseException, dataset_id: int) -> bool:
+    """Use the dataset's DB engine spec to classify the error.
+
+    Walks the ``__cause__`` chain for direct ``SQLAlchemyError`` instances,
+    then falls back to the engine spec's ``extract_errors`` regex patterns —
+    the same classification the Superset UI uses.
+    """
+    # Direct SQLAlchemy errors (unwrapped or in cause chain)
+    current: BaseException | None = exc
+    while current is not None:
+        if isinstance(current, SQLAlchemyError):
+            return True
+        current = current.__cause__
+
+    # Use the dataset's engine spec to classify (same as the UI)
+    try:
+        from superset.daos.dataset import DatasetDAO
+
+        dataset = DatasetDAO.find_by_id(dataset_id)
+        if dataset and dataset.database and isinstance(exc, Exception):
+            errors = dataset.database.db_engine_spec.extract_errors(exc)
+            return any(e.error_type in _CONNECTION_ERROR_TYPES for e in errors)
+    except Exception:  # pylint: disable=broad-except
+        logger.debug(
+            "Failed to classify error via engine spec for dataset %s: %s",
+            dataset_id,
+            exc,
+        )
+
+    return False
+
+
+def _build_database_error(message: str) -> ChartGenerationError:
+    """Wrap a database connection failure in the structured response 
envelope."""
+    return ChartGenerationError(
+        error_type="database_connection_error",
+        message="Unable to connect to the database.",
+        details=message or "",
+        suggestions=[
+            "Check that the database is online and reachable",
+            "Verify database credentials and connection settings",
+            "Contact your administrator if the issue persists",
+        ],
+        error_code="DATABASE_CONNECTION_ERROR",
+    )
+
+
 def _build_compile_error(message: str) -> ChartGenerationError:
     """Wrap a raw compile-failure string in the structured response 
envelope."""
     return ChartGenerationError(
diff --git a/superset/mcp_service/chart/tool/generate_chart.py 
b/superset/mcp_service/chart/tool/generate_chart.py
index 45f5d30a17a..caa6db7e41c 100644
--- a/superset/mcp_service/chart/tool/generate_chart.py
+++ b/superset/mcp_service/chart/tool/generate_chart.py
@@ -416,7 +416,7 @@ async def generate_chart(  # noqa: C901
                     )
 
                     execution_time = int((time.time() - start_time) * 1000)
-                    error = ChartGenerationError(
+                    error = compile_result.error_obj or ChartGenerationError(
                         error_type="compile_error",
                         message=(
                             "Chart query failed to execute. The chart was not 
saved."
@@ -546,7 +546,7 @@ async def generate_chart(  # noqa: C901
                     )
 
                     execution_time = int((time.time() - start_time) * 1000)
-                    error = ChartGenerationError(
+                    error = compile_result.error_obj or ChartGenerationError(
                         error_type="compile_error",
                         message=(
                             "Chart query failed to execute. "
diff --git a/tests/unit_tests/mcp_service/chart/test_compile.py 
b/tests/unit_tests/mcp_service/chart/test_compile.py
index 4c52063e5de..e65e6b801d4 100644
--- a/tests/unit_tests/mcp_service/chart/test_compile.py
+++ b/tests/unit_tests/mcp_service/chart/test_compile.py
@@ -423,6 +423,93 @@ class TestValidateAndCompileTier2:
         assert result.error_code == "DATASET_NOT_FOUND"
 
 
+@patch("superset.daos.dataset.DatasetDAO")
+@patch("superset.commands.chart.data.get_data_command.ChartDataCommand")
+@patch("superset.common.query_context_factory.QueryContextFactory")
+def test_compile_chart_returns_database_error_when_wrapped_in_query_failed(
+    mock_factory, mock_cmd_cls, mock_dataset_dao
+):
+    """ChartDataCommand converts OperationalError to a string inside
+    ChartDataQueryFailedError (no __cause__ set). _classify_as_database_error
+    should use db_engine_spec.extract_errors() to detect the DB error."""
+    from superset.commands.chart.exceptions import ChartDataQueryFailedError
+    from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
+    from superset.mcp_service.chart.compile import _compile_chart
+
+    mock_factory.return_value.create.return_value = Mock()
+    mock_cmd_cls.return_value.validate.return_value = None
+
+    # Real scenario: __cause__ is NOT set, error is just a string
+    wrapped = ChartDataQueryFailedError(
+        "Error: (psycopg2.OperationalError) connection to server at 
'10.0.0.1',"
+        " port 5432 failed: FATAL: tenant not found"
+    )
+    mock_cmd_cls.return_value.run.side_effect = wrapped
+
+    # Mock the dataset's db_engine_spec to return GENERIC_DB_ENGINE_ERROR
+    mock_db = Mock()
+    mock_db.db_engine_spec.extract_errors.return_value = [
+        SupersetError(
+            error_type=SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
+            message="connection to server failed",
+            level=ErrorLevel.ERROR,
+            extra={"engine_name": "PostgreSQL"},
+        )
+    ]
+    mock_dataset = Mock()
+    mock_dataset.database = mock_db
+    mock_dataset_dao.find_by_id.return_value = mock_dataset
+
+    result = _compile_chart(
+        form_data={
+            "metrics": [{"label": "count", "expressionType": "SIMPLE"}],
+            "adhoc_filters": [],
+        },
+        dataset_id=1,
+    )
+
+    assert not result.success
+    assert "Database connection error" in result.error
+    assert result.error_obj is not None
+    assert result.error_obj.error_type == "database_connection_error"
+    assert result.error_obj.error_code == "DATABASE_CONNECTION_ERROR"
+    mock_db.db_engine_spec.extract_errors.assert_called_once()
+
+
+@patch("superset.commands.chart.data.get_data_command.ChartDataCommand")
+@patch("superset.common.query_context_factory.QueryContextFactory")
+def test_compile_chart_returns_database_error_on_raw_sqlalchemy_error(
+    mock_factory, mock_cmd_cls
+):
+    """When SQLAlchemyError escapes unwrapped, _compile_chart should
+    catch it and return a database_connection_error."""
+    from sqlalchemy.exc import OperationalError
+
+    from superset.mcp_service.chart.compile import _compile_chart
+
+    mock_factory.return_value.create.return_value = Mock()
+    mock_cmd_cls.return_value.validate.return_value = None
+    mock_cmd_cls.return_value.run.side_effect = OperationalError(
+        "connection to server at '10.0.0.1', port 5432 failed: Connection 
timed out",
+        None,
+        None,
+    )
+
+    result = _compile_chart(
+        form_data={
+            "metrics": [{"label": "count", "expressionType": "SIMPLE"}],
+            "adhoc_filters": [],
+        },
+        dataset_id=1,
+    )
+
+    assert not result.success
+    assert "Database connection error" in result.error
+    assert result.error_obj is not None
+    assert result.error_obj.error_type == "database_connection_error"
+    assert result.error_obj.error_code == "DATABASE_CONNECTION_ERROR"
+
+
 @pytest.mark.parametrize(
     "config_factory",
     [

Reply via email to