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

rusackas pushed a commit to branch test/oracle-cancel-query-testcontainers
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 1107f358243b3cdf198a5000df0f8304d6b6eb2c
Author: Evan Rusackas <[email protected]>
AuthorDate: Fri Oct 2 14:37:16 2026 -0700

    test(oracle): cover cancel-query against a real running statement
    
    apache/superset#44704 added OracleEngineSpec.get_cancel_query_id/
    cancel_query, verified only by hand against a real Oracle instance
    during development per that PR's own testing notes -- never pinned
    down as a repeatable test. Exercises the real pair against a real
    session: identify the session about to run a long statement on one
    connection, cancel it from a second connection mid-flight (mirroring
    SQL Lab's own cancel path), and confirm the statement actually fails
    with ORA-01013 well before it would complete on its own.
    
    Co-Authored-By: Evan Rusackas <[email protected]>
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
---
 .../testcontainers/db_engine_specs/test_oracle.py  | 86 ++++++++++++++++++++++
 1 file changed, 86 insertions(+)

diff --git a/tests/testcontainers/db_engine_specs/test_oracle.py 
b/tests/testcontainers/db_engine_specs/test_oracle.py
index 93020613c56..1a902cc5fbb 100644
--- a/tests/testcontainers/db_engine_specs/test_oracle.py
+++ b/tests/testcontainers/db_engine_specs/test_oracle.py
@@ -25,7 +25,10 @@ under 15s measured locally. Almost all the wall-clock cost 
here is the
 image pull itself, same as any other dialect's container.
 """
 
+import threading
+import time
 from collections.abc import Iterator
+from typing import Any
 
 import pytest
 from sqlalchemy import (
@@ -39,6 +42,7 @@ from sqlalchemy import (
 from sqlalchemy.engine import Engine
 
 from superset.db_engine_specs.oracle import OracleEngineSpec
+from superset.models.sql_lab import Query
 from superset.sql.parse import Table
 
 pytestmark = pytest.mark.testcontainers
@@ -93,3 +97,85 @@ def test_get_columns_maps_native_types(engine: Engine) -> 
None:
     for col in by_name.values():
         spec = OracleEngineSpec.get_column_spec(str(col["type"]))
         assert spec is not None
+
+
+def test_cancel_query_stops_a_running_statement(engine: Engine) -> None:
+    """
+    apache/superset#44704 added OracleEngineSpec.get_cancel_query_id/
+    cancel_query, verified only by hand against a real Oracle instance
+    during development (per that PR's own testing notes) -- never pinned
+    down as a repeatable test. This exercises the real pair against a real
+    session: identify the session about to run a long statement on one
+    connection, cancel it from a second connection mid-flight (mirroring
+    SQL Lab's own "new cursor to the db of the query" cancel path), and
+    confirm the first connection's statement actually fails with ORA-01013
+    well before it would complete on its own -- not just that
+    ``cancel_query`` returned ``True`` without effect.
+
+    The container's default user is ``system`` (DBA), which holds the
+    ``ALTER SYSTEM`` privilege ``cancel_query`` needs; see
+    apache/superset#44704's description for the ``False``/ORA-01031 path
+    when that privilege is absent, which this does not exercise.
+    """
+    query_conn = engine.raw_connection()
+    cancel_conn = engine.raw_connection()
+    try:
+        query_cursor = query_conn.cursor()
+        # get_cancel_query_id runs on the same cursor that is about to
+        # execute the query, before it does -- mirrors SQL Lab's own order.
+        cancel_query_id = OracleEngineSpec.get_cancel_query_id(query_cursor, 
Query())
+        assert cancel_query_id is not None
+
+        outcome: dict[str, Any] = {}
+
+        def run_slow_query() -> None:
+            try:
+                # A tight PL/SQL loop: CPU-bound with O(1) memory, needs no
+                # privileges beyond CREATE SESSION, and (measured directly
+                # against this same image) ~8s at 500M iterations -- plenty
+                # of margin over the 1.5s delay below. Two things this is
+                # NOT, both tried first:
+                #   - `CONNECT BY LEVEL <= n`: materializes the whole
+                #     hierarchy and hits ORA-30009 "not enough memory" long
+                #     before a count this large finishes -- that failure has
+                #     nothing to do with cancellation, so the test would pass
+                #     for the wrong reason.
+                #   - a loop with no loop-carried side effect (`NULL;` as the
+                #     body): optimized away entirely regardless of the
+                #     iteration count, so the statement returns instantly and
+                #     the cancel never has anything to catch. The `cnt`
+                #     assignment here is load-bearing, not cosmetic.
+                query_cursor.execute(
+                    "DECLARE cnt NUMBER := 0; BEGIN "
+                    "FOR i IN 1..500000000 LOOP cnt := cnt + 1; END LOOP; "
+                    "END;"
+                )
+                outcome["completed"] = True
+            except Exception as ex:  # noqa: BLE001  # pylint: 
disable=broad-except
+                outcome["error"] = ex
+
+        thread = threading.Thread(target=run_slow_query)
+        start = time.monotonic()
+        thread.start()
+        time.sleep(1.5)  # let the statement actually start executing
+
+        cancel_cursor = cancel_conn.cursor()
+        cancelled = OracleEngineSpec.cancel_query(
+            cancel_cursor, Query(), cancel_query_id
+        )
+        assert cancelled is True
+
+        thread.join(timeout=60)
+        elapsed = time.monotonic() - start
+
+        assert not thread.is_alive(), (
+            "cancel_query returned True but the statement is still running"
+        )
+        assert "error" in outcome, "the statement completed instead of being 
cancelled"
+        assert "ORA-01013" in str(outcome["error"])
+        # The cancel took effect promptly, not "eventually" after the slow
+        # statement would have finished on its own regardless.
+        assert elapsed < 30
+    finally:
+        query_conn.close()
+        cancel_conn.close()

Reply via email to