You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ri...@apache.org on 2026/09/17 18:27:55 UTC
(superset) branch master updated: fix(versioning): scope stream_results to the activity SELECTs instead of mutating the session connection (#44261)
This is an automated email from the ASF dual-hosted git repository.
richardfogaca 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 c426186a2e6 fix(versioning): scope stream_results to the activity SELECTs instead of mutating the session connection (#44261)
c426186a2e6 is described below
commit c426186a2e664301dfa64400bc4a9ac1ec35b784
Author: Mike Bridge <mi...@preset.io>
AuthorDate: Thu Sep 17 12:27:47 2026 -0600
fix(versioning): scope stream_results to the activity SELECTs instead of mutating the session connection (#44261)
Co-authored-by: Claude Fable 5 <no...@anthropic.com>
---
superset/versioning/activity/queries.py | 11 +-
.../versioning/activity_write_after_read_tests.py | 124 +++++++++++++++++++++
2 files changed, 133 insertions(+), 2 deletions(-)
diff --git a/superset/versioning/activity/queries.py b/superset/versioning/activity/queries.py
index 5c6476b65e1..e29c242baff 100644
--- a/superset/versioning/activity/queries.py
+++ b/superset/versioning/activity/queries.py
@@ -418,10 +418,17 @@ def _select_change_rows_for_kinds(
vc.c.sequence.desc(),
vc.c.entity_id.desc(),
)
+ # stream_results rides the STATEMENT, never the connection:
+ # ``Connection.execution_options`` mutates the session's
+ # connection in place, permanently flipping every later
+ # statement on the request into a server-side cursor — on
+ # PostgreSQL that wraps subsequent INSERTs/SAVEPOINTs in
+ # ``DECLARE ... CURSOR FOR`` and they fail with a syntax
+ # error (observed: Continuum's transaction insert and the
+ # DBEventLogger write after an activity read; sc-120955).
result = (
db.session.connection()
- .execution_options(stream_results=True)
- .execute(stmt)
+ .execute(stmt.execution_options(stream_results=True))
.mappings()
)
ordinal = _merge_result_into_heap(
diff --git a/tests/integration_tests/versioning/activity_write_after_read_tests.py b/tests/integration_tests/versioning/activity_write_after_read_tests.py
new file mode 100644
index 00000000000..8ef1790dee4
--- /dev/null
+++ b/tests/integration_tests/versioning/activity_write_after_read_tests.py
@@ -0,0 +1,124 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""sc-120955: an activity read must not poison the session for later
+writes. ``fetch_change_records`` used to flip the session connection
+into server-side-cursor mode in place; on PostgreSQL every subsequent
+INSERT/SAVEPOINT on the request then failed with a syntax error
+(``DECLARE ... CURSOR FOR INSERT``) — the DBEventLogger write after any
+activity read being the production shape."""
+
+from __future__ import annotations
+
+from typing import Any
+
+from flask_appbuilder.security.sqla.models import User
+from sqlalchemy import event
+from sqlalchemy.engine import Connection, ExecutionContext
+
+from superset.extensions import db
+from superset.models.slice import Slice
+from superset.utils.core import override_user
+from superset.versioning.activity.orchestrator import get_activity
+from tests.integration_tests.base_tests import SupersetTestCase
+
+
+def _admin_user() -> User:
+ # pylint: disable=import-outside-toplevel
+ from superset import security_manager
+
+ return (
+ db.session.query(security_manager.user_model)
+ .filter_by(username="admin")
+ .one_or_none()
+ ) or security_manager.add_user(
+ "admin",
+ "admin",
+ "user",
+ "admin@fab.org",
+ security_manager.find_role("Admin"),
+ password="general", # noqa: S106 — test-only fixture credential
+ )
+
+
+class TestActivityReadThenWrite(SupersetTestCase):
+ def test_write_on_the_same_session_after_an_activity_read(self) -> None:
+ """The DBEventLogger shape: read activity, then INSERT + commit on
+ the SAME session. With the connection mutated into stream_results
+ mode this failed on PostgreSQL before any assertion ran."""
+ slc: Slice = Slice(
+ slice_name="sc120955_read_then_write",
+ datasource_type="table",
+ datasource_id=1,
+ params="{}",
+ )
+ db.session.add(slc)
+ db.session.commit()
+ try:
+ # The read that used to poison the connection. The entity has
+ # history (its own INSERT), so the streaming fetch executes.
+ slc.slice_name = "sc120955_read_then_write_edited"
+ db.session.commit()
+ streamed_cursors: list[str | None] = []
+
+ def observe_change_select(
+ connection: Connection,
+ cursor: Any,
+ statement: str,
+ parameters: Any,
+ context: ExecutionContext,
+ executemany: bool,
+ ) -> None:
+ """Observe the change SELECT even if streaming is removed."""
+ if statement.lstrip().upper().startswith("SELECT") and (
+ "version_changes" in statement
+ ):
+ streamed_cursors.append(getattr(cursor, "name", None))
+
+ records: list[dict[str, Any]]
+ event.listen(db.engine, "before_cursor_execute", observe_change_select)
+ try:
+ with override_user(_admin_user()):
+ records, _, _ = get_activity(Slice, slc.uuid, resolved_entity=slc)
+ finally:
+ event.remove(db.engine, "before_cursor_execute", observe_change_select)
+ assert records is not None
+ assert streamed_cursors, "The activity read must execute its change SELECT"
+ if db.engine.dialect.name == "postgresql":
+ assert all(streamed_cursors), "Activity SELECTs must use named cursors"
+ assert (
+ not db.session.connection()
+ .get_execution_options()
+ .get("stream_results", False)
+ )
+
+ # A write through the same session must still work — this is
+ # what Continuum's transaction INSERT, SAVEPOINTs, and the
+ # DBEventLogger all do after the endpoint's read.
+ follower: Slice = Slice(
+ slice_name="sc120955_follow_up_write",
+ datasource_type="table",
+ datasource_id=1,
+ params="{}",
+ )
+ db.session.add(follower)
+ db.session.commit()
+ assert follower.id is not None
+ db.session.delete(follower)
+ db.session.commit()
+ finally:
+ db.session.delete(slc)
+ db.session.commit()