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/20 21:31:21 UTC

(superset) branch master updated: fix(sqllab): commit the recomputed row limit before async dispatch (#45178)

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

sadpandajoe 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 77ded19ba5c fix(sqllab): commit the recomputed row limit before async dispatch (#45178)
77ded19ba5c is described below

commit 77ded19ba5c68aa3c4c235111419f11b6e61272e
Author: Joe Li <jo...@preset.io>
AuthorDate: Tue Oct 20 14:31:14 2026 -0700

    fix(sqllab): commit the recomputed row limit before async dispatch (#45178)
---
 superset/commands/sql_lab/execute.py              | 12 +++
 tests/unit_tests/commands/sql_lab/test_execute.py | 92 +++++++++++++++++++++++
 2 files changed, 104 insertions(+)

diff --git a/superset/commands/sql_lab/execute.py b/superset/commands/sql_lab/execute.py
index 99ed73fdef7..e2ada480438 100644
--- a/superset/commands/sql_lab/execute.py
+++ b/superset/commands/sql_lab/execute.py
@@ -164,6 +164,18 @@ class ExecuteSqlCommand(BaseCommand):
             self._query_dao.update(
                 query, {"limit": self._execution_context.query.limit}
             )
+            # Commit now, before dispatch. The async executor below hands off to
+            # a Celery worker that re-fetches this query on its own connection
+            # (`get_query` in superset/sql_lab.py); under READ COMMITTED that
+            # worker only sees the limit recomputed just above once it is
+            # committed. `@transaction()` on `run()` only commits after this
+            # whole method returns, which is too late: `.delay()` can hand the
+            # task to an already-idle worker before that commit lands, so the
+            # worker reads back the row's limit as it was at creation time (the
+            # raw dropdown value from `create_query()`) instead of the smaller
+            # value just computed from the query's own SQL LIMIT. Same race and
+            # fix as apache/superset#40070.
+            db.session.commit()  # pylint: disable=consider-using-transaction
             return self._sql_json_executor.execute(
                 self._execution_context, rendered_query, self._log_params
             )
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()