I3eka commented on code in PR #45561:
URL: https://github.com/apache/superset/pull/45561#discussion_r4335880497


##########
superset/sql/execution/executor.py:
##########
@@ -577,6 +579,7 @@ def execute(
 
         opts: QueryOptionsType = options or QueryOptionsType()
         start_time = time.time()
+        query: Query | None = None

Review Comment:
   This does not raise `NameError`. The module already has `from __future__ 
import annotations` at line 57, unchanged from master. Function-local variable 
annotations are also not evaluated at runtime, even without that future import. 
I verified the module imports with `Query` absent from its runtime globals, and 
the execution tests exercise this line successfully. Keeping the type-only 
import.



##########
superset/sql/execution/executor.py:
##########
@@ -1166,7 +1203,7 @@ def _create_query_record(
         catalog: str | None,
         schema: str | None,
         status: QueryStatus,
-    ) -> Any:
+    ) -> Query:

Review Comment:
   Postponed annotation evaluation is already enabled by `from __future__ 
import annotations` at line 57. I checked this directly: 
`SQLExecutor._create_query_record.__annotations__["return"]` is the string 
`"Query"`, and the module imports with no runtime `Query` binding. There is no 
runtime lookup here, so no change is needed.



##########
superset/sql/execution/executor.py:
##########
@@ -1008,14 +1020,39 @@ def _create_error_result(
         :param start_time: Start time for calculating execution duration
         :param partial_results: Optional list of StatementResult from 
successful
             statements before the failure
+        :param query: Persisted audit record, if execution reached that stage
         :returns: QueryResult with error status
         """
-        from superset_core.queries.types import QueryResult as QueryResultType
+        from superset_core.queries.types import (
+            QueryResult as QueryResultType,
+            QueryStatus,
+        )
+
+        from superset.sql.execution.utils import commit_query_unless_stopped
+
+        query_id = None
+        if query is not None:
+            try:
+                # A failed metadata flush must be rolled back before recording 
failure.
+                db.session.rollback()  # pylint: 
disable=consider-using-transaction
+                query_id = query.id

Review Comment:
   Confirmed and fixed in `85257270c9`. A regression using real SQLite sessions 
reproduced the loss of another row's pending edit and a pending insert on SQL 
failure. Finalization now rolls back first only when `db.session.is_active` is 
false. The new tests verify that healthy pending work survives and that a real 
failed metadata flush is still recovered. The OAuth-error and concurrent-Stop 
regressions also pass.



##########
tests/unit_tests/sql/execution/test_executor.py:
##########
@@ -763,6 +764,209 @@ def test_execute_custom_timeout(
     )
 
 
[email protected]("failure", ["database", "timeout", "oauth"])
+def test_execute_failure_finishes_query_record(
+    mocker: MockerFixture,
+    database: Database,
+    app_context: None,
+    mock_query: MagicMock,
+    mock_db_session: MagicMock,
+    default_sql_config: None,
+    failure: str,
+) -> None:
+    """An execution failure must not leave its audit record running."""
+    from superset.errors import ErrorLevel, SupersetErrorType
+    from superset.exceptions import OAuth2Error, SupersetTimeoutException
+    from superset.sql.execution.executor import SQLExecutor
+
+    errors = {
+        "database": RuntimeError("Query failed"),
+        "timeout": SupersetTimeoutException(
+            error_type=SupersetErrorType.GENERIC_BACKEND_ERROR,
+            message="Query timed out",
+            level=ErrorLevel.ERROR,
+        ),
+        "oauth": OAuth2Error("Authentication required"),
+    }
+    mock_query.status = "running"
+    mocker.patch.object(SQLExecutor, "_create_query_record", 
return_value=mock_query)
+    mocker.patch.object(SQLExecutor, "_try_get_cached_result", 
return_value=None)
+    mocker.patch.object(SQLExecutor, "_execute_statements", 
side_effect=errors[failure])
+    mocker.patch(
+        "superset.sql.execution.executor.now_as_float",
+        return_value=1234000.0,
+        create=True,
+    )

Review Comment:
   `create=True` is intentional here: this regression also runs against 
unmodified upstream, where the executor did not import `now_as_float`. It lets 
the test reach the stale-query-status failure instead of failing while setting 
up the patch. A wrong patch target cannot make this test pass: `assert 
mock_query.end_time == 1234000.0` requires the patched clock value to reach the 
audit record. Keeping it for that regression use case.



##########
tests/unit_tests/sql/execution/test_executor.py:
##########
@@ -763,6 +764,209 @@ def test_execute_custom_timeout(
     )
 
 
[email protected]("failure", ["database", "timeout", "oauth"])
+def test_execute_failure_finishes_query_record(
+    mocker: MockerFixture,
+    database: Database,
+    app_context: None,
+    mock_query: MagicMock,
+    mock_db_session: MagicMock,
+    default_sql_config: None,
+    failure: str,
+) -> None:
+    """An execution failure must not leave its audit record running."""
+    from superset.errors import ErrorLevel, SupersetErrorType
+    from superset.exceptions import OAuth2Error, SupersetTimeoutException
+    from superset.sql.execution.executor import SQLExecutor
+
+    errors = {
+        "database": RuntimeError("Query failed"),
+        "timeout": SupersetTimeoutException(
+            error_type=SupersetErrorType.GENERIC_BACKEND_ERROR,
+            message="Query timed out",
+            level=ErrorLevel.ERROR,
+        ),
+        "oauth": OAuth2Error("Authentication required"),
+    }
+    mock_query.status = "running"
+    mocker.patch.object(SQLExecutor, "_create_query_record", 
return_value=mock_query)
+    mocker.patch.object(SQLExecutor, "_try_get_cached_result", 
return_value=None)
+    mocker.patch.object(SQLExecutor, "_execute_statements", 
side_effect=errors[failure])
+    mocker.patch(
+        "superset.sql.execution.executor.now_as_float",
+        return_value=1234000.0,
+        create=True,
+    )
+    expected_status = (
+        QueryStatus.TIMED_OUT if failure == "timeout" else QueryStatus.FAILED
+    )
+
+    if failure == "oauth":
+        with pytest.raises(OAuth2Error):
+            database.execute("SELECT 1")
+    else:
+        result = database.execute("SELECT 1")
+        assert result.status == expected_status
+        assert result.query_id == mock_query.id
+        assert result.error_message == mock_query.error_message
+
+    assert mock_query.status == expected_status.value
+    assert mock_query.error_message
+    assert mock_query.end_time == 1234000.0
+    mock_db_session.rollback.assert_called_once()
+    mock_db_session.commit.assert_called_once()
+
+
[email protected]("failure", ["database", "timeout", "oauth", 
"oauth_redirect"])
[email protected]("metadata_failure", ["rollback", "commit"])
+def test_execute_preserves_error_when_failure_recording_fails(
+    mocker: MockerFixture,
+    database: Database,
+    app_context: None,
+    mock_query: MagicMock,
+    mock_db_session: MagicMock,
+    default_sql_config: None,
+    failure: str,
+    metadata_failure: str,
+    caplog: pytest.LogCaptureFixture,
+) -> None:
+    """A metadata outage must not replace the execution error or OAuth 
redirect."""
+    from superset.errors import ErrorLevel, SupersetErrorType
+    from superset.exceptions import (
+        OAuth2Error,
+        OAuth2RedirectError,
+        SupersetTimeoutException,
+    )
+    from superset.sql.execution.executor import SQLExecutor
+
+    error = {
+        "database": RuntimeError("Query failed"),
+        "timeout": SupersetTimeoutException(
+            error_type=SupersetErrorType.GENERIC_BACKEND_ERROR,
+            message="Query timed out",
+            level=ErrorLevel.ERROR,
+        ),
+        "oauth": OAuth2Error("Authentication required"),
+        "oauth_redirect": OAuth2RedirectError(
+            "https://example.com/auth";, "tab-id", 
"https://example.com/callback";
+        ),
+    }[failure]
+    mocker.patch.object(SQLExecutor, "_create_query_record", 
return_value=mock_query)
+    mocker.patch.object(SQLExecutor, "_try_get_cached_result", 
return_value=None)
+    mocker.patch.object(SQLExecutor, "_execute_statements", side_effect=error)
+    getattr(mock_db_session, metadata_failure).side_effect = RuntimeError(
+        "Metadata unavailable"
+    )
+
+    if isinstance(error, (OAuth2Error, OAuth2RedirectError)):
+        with pytest.raises(type(error)) as caught:
+            database.execute("SELECT 1")
+        assert caught.value is error
+    else:
+        result = database.execute("SELECT 1")
+        assert result.status == (
+            QueryStatus.TIMED_OUT if failure == "timeout" else 
QueryStatus.FAILED
+        )
+        assert result.error_message == (
+            "Query exceeded the timeout limit"
+            if failure == "timeout"
+            else "sqlite error: Query failed"
+        )
+        if metadata_failure == "commit":
+            assert result.query_id == mock_query.id
+
+    assert mock_db_session.rollback.call_count == 2
+    assert "Failed to persist SQL query error" in caplog.text

Review Comment:
   Confirmed as a test reliability issue. Running these eight cases with 
`--log-level=ERROR` reproduced eight failures because the warning was absent 
from `caplog.text`. Fixed in `85257270c9` by explicitly setting WARNING capture 
for `superset.sql.execution.executor`. All eight cases now pass with the global 
level still set to ERROR.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to