You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by jl...@apache.org on 2026/10/10 19:35:05 UTC

(superset) 01/02: test(sqllab): add a RED regression guard for the async limit-commit race

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

sadpandajoe pushed a commit to branch fix-sqllab-async-limit-race
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 41a96d3cd971bcfdf9df5d2092eec49a1857fd82
Author: Joe Li <jo...@preset.io>
AuthorDate: Sat Oct 10 19:30:48 2026 +0000

    test(sqllab): add a RED regression guard for the async limit-commit race
    
    ExecuteSqlCommand recomputes query.limit as min(the query's own SQL
    LIMIT, the row-limit dropdown) and updates it in place, but never
    commits before the async executor can dispatch to a Celery worker that
    re-fetches the row on its own connection. Pins the ordering with a
    commit-time value recorder; fails today because no commit observes
    the corrected limit before dispatch. Same mechanism as #40070.
---
 tests/unit_tests/commands/sql_lab/test_execute.py | 92 +++++++++++++++++++++++
 1 file changed, 92 insertions(+)

diff --git a/tests/unit_tests/commands/sql_lab/test_execute.py b/tests/unit_tests/commands/sql_lab/test_execute.py
index dabda8f6d48..70ae8bde04a 100644
--- a/tests/unit_tests/commands/sql_lab/test_execute.py
+++ b/tests/unit_tests/commands/sql_lab/test_execute.py
@@ -159,3 +159,95 @@ def test_run_sql_json_exec_from_scratch_revalidates_rendered_sql(
     sql_json_executor.execute.assert_called_once_with(
         execution_context, "SELECT * FROM sales", None
     )
+
+
+# ---------------------------------------------------------------------------
+# Regression: the recomputed row limit must be committed before the async
+# executor can race it (apache/superset#40070)
+# ---------------------------------------------------------------------------
+
+
+class _FakeQuery:
+    """
+    A minimal stand-in whose ``.limit`` behaves like the real ORM attribute,
+    so a commit's side effect can observe its *live* value. A plain
+    ``MagicMock`` attribute can't distinguish "assigned in memory" from
+    "actually committed" the way this test needs to.
+    """
+
+    def __init__(self, limit: int) -> None:
+        self.id = 1
+        self.limit = limit
+        self.select_as_cta = False
+        self.executed_sql: str | None = None
+
+
+@patch("superset.commands.sql_lab.execute.db")
+def test_run_sql_json_exec_from_scratch_commits_recomputed_limit_before_dispatch(
+    mock_db: MagicMock,
+) -> None:
+    """
+    Regression for apache/superset#40070 (same mechanism, confirmed by a
+    community member there, never fixed).
+
+    ``_set_query_limit`` recomputes ``query.limit`` as ``min(sql_limit,
+    dropdown_limit)`` and the DAO ``update`` call assigns it in place, but
+    neither commits. For an async (Celery) query, ``sql_json_executor.execute``
+    below is ``ASynchronousSqlJsonExecutor.execute`` -- it hands the query off
+    via ``.delay()`` to a worker that re-fetches the row on its own
+    connection (``get_query`` in ``superset/sql_lab.py``). Under READ
+    COMMITTED, that worker only sees the recomputed limit once it is
+    actually committed; otherwise it reads the row as it was at creation
+    time -- the raw dropdown value written by ``create_query()``, which is
+    always >= the correct minimum. That is exactly "the dropdown wins" even
+    though the query's own LIMIT was smaller.
+
+    A real Celery worker isn't needed to pin this: it is enough to prove the
+    commit boundary is in the right place -- that by the time the executor
+    is invoked, a commit has already been observed with the *corrected*
+    limit, not just the row's original creation-time value.
+    """
+    query = _FakeQuery(limit=1000)  # raw dropdown value, as written by create_query()
+
+    observed_limits_at_commit: list[int] = []
+    mock_db.session.commit.side_effect = lambda: observed_limits_at_commit.append(
+        query.limit
+    )
+
+    execution_context = MagicMock()
+    execution_context.template_params = {}
+    execution_context.select_as_cta = False
+    execution_context.limit = 1000  # the dropdown value the user chose
+    execution_context.create_query.return_value = query
+    execution_context.query = query
+    execution_context.database.db_engine_spec.get_limit_from_sql.return_value = 200
+
+    database_dao = MagicMock()
+    database_dao.find_by_id.return_value = MagicMock()
+
+    sql_query_render = MagicMock()
+    sql_query_render.render.return_value = "SELECT * FROM sales LIMIT 200"
+
+    def _fake_execute(*_args: object, **_kwargs: object) -> SqlJsonExecutionStatus:
+        assert 200 in observed_limits_at_commit, (
+            "the recomputed row limit (min of the query's own LIMIT and the "
+            "dropdown) must be committed before the async executor dispatches "
+            "to a Celery worker, or a worker racing the dispatch can read "
+            "back the stale pre-recompute (dropdown) value instead"
+        )
+        return SqlJsonExecutionStatus.QUERY_IS_RUNNING
+
+    sql_json_executor = MagicMock()
+    sql_json_executor.execute.side_effect = _fake_execute
+
+    command = _make_command(
+        execution_context=execution_context,
+        database_dao=database_dao,
+        sql_query_render=sql_query_render,
+        sql_json_executor=sql_json_executor,
+    )
+
+    command._run_sql_json_exec_from_scratch()
+
+    assert query.limit == 200
+    sql_json_executor.execute.assert_called_once()