Skip to content

Fix BEGIN/COMMIT/ROLLBACK silently failing under server cursor mode - #10321

Open
dpage wants to merge 2 commits into
pgadmin-org:masterfrom
dpage:fix/8991-servercursor-commit-rollback
Open

Fix BEGIN/COMMIT/ROLLBACK silently failing under server cursor mode#10321
dpage wants to merge 2 commits into
pgadmin-org:masterfrom
dpage:fix/8991-servercursor-commit-rollback

Conversation

@dpage

@dpage dpage commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Reported as a UI glitch (the result grid stays visible instead of
switching to the Messages tab after Commit/Rollback with "server
cursor" mode on), but the root cause is more serious: under server
cursor mode, BEGIN/COMMIT/ROLLBACK never actually reached the database.

execute_void() reuses whatever cursor is cached on the connection,
which under server cursor mode is the named/server-side cursor left
over from the last SELECT. A named cursor's execute() always wraps
the statement as DECLARE ... CURSOR FOR <query>, which can't express
a transaction-control statement (DECLARE ... CURSOR FOR COMMIT is a
syntax error) — and it actually failed one step earlier still, on a
prepare= keyword the server-side cursor's execute() doesn't accept
at all (TypeError: keyword not supported: prepare). That exception
was swallowed by a blanket except Exception in the background query
thread, so the statement silently never ran, leaving the transaction
open with no error shown to the user. The empty grid was just the
visible fallout: the next /poll picked up the previous query's
leftover column info instead of reporting "no result set".

This routes BEGIN/COMMIT/ROLLBACK through a throwaway plain cursor
instead of the cached server-side one when server cursor mode is
active, and clears the stale column info so poll() correctly reports
no result set afterwards.

Fixes #8991.

Test plan

  • Verified the failure mode directly against a live PostgreSQL 18
    connection (both the prepare TypeError and the underlying
    DECLARE ... CURSOR FOR COMMIT syntax error), and confirmed the fix's
    approach (a plain connection.cursor() alongside an open named
    cursor) commits correctly and reports description is None
    afterwards.
  • Added test_execute_void_server_cursor.py, covering COMMIT and
    ROLLBACK with a cached server-side cursor: asserts the statement runs
    on a plain cursor (not the cached server one) and that stale column
    info/row count are cleared. Confirmed it fails without the fix and
    passes with it.
  • python regression/runtests.py --pkg utils.driver.psycopg3.tests.test_execute_void_server_cursor
    passes.

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 45 minutes.

View limit details

Limit details: You’ve used all 8 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 44db859d-4c78-41fd-903d-5409c469c9d3

📥 Commits

Reviewing files that changed from the base of the PR and between bc58657 and e70c698.

📒 Files selected for processing (4)
  • web/pgadmin/tools/sqleditor/__init__.py
  • web/pgadmin/tools/sqleditor/tests/test_poll_explain_query_length_guard.py
  • web/pgadmin/utils/driver/psycopg3/connection.py
  • web/pgadmin/utils/driver/psycopg3/tests/test_execute_void_server_cursor.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hiteshjambhale hiteshjambhale left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found following issue while testing:

get_explain_query_length crashes with AttributeError: 'NoneType' object has no attribute 'query' on repeated Commit under server-cursor mode
Reproducible on current master.

Steps to reproduce:

  1. Open a Query Tool.
  2. In the Execute Options (▾ next to Execute): turn "Use server cursor?" ON and "Auto commit?" OFF.
  3. Run any SELECT (e.g. SELECT 1;) — status bar shows "executed with server cursor".
  4. Click execute.
  5. Click execute again
  6. Or try running any other query again

.
Result: 500 error — AttributeError: 'NoneType' object has no attribute 'query'; the Query Tool becomes unusable (Execute Options dropdown greyed out).

dpage added 2 commits August 25, 2026 09:51
…mode (pgadmin-org#8991)

execute_void() blindly reused whatever cursor was cached for the
connection, which under "server cursor" mode is the named/server-side
AsyncDictServerCursor left over from the last SELECT. A named cursor's
execute() always wraps the statement as `DECLARE ... CURSOR FOR
<query>`, which cannot express a transaction-control statement, so
BEGIN/COMMIT/ROLLBACK silently failed (failing one step earlier still,
on a `prepare` keyword the server-side cursor's execute() doesn't
accept at all) and the exception was swallowed by the background query
thread. The transaction was therefore never actually committed or
rolled back, and the next poll() picked up the previous query's
leftover column info, which is what made the result grid appear
instead of the Messages tab.

Run the statement through a throwaway plain cursor instead, leaving
the cached server-side cursor untouched, and clear the stale column
info so poll() correctly reports no result set.
… yet

Under server cursor mode, execute_void() running BEGIN/COMMIT/ROLLBACK
on a throwaway plain cursor can leave the cached async cursor pointing
at a cursor that has not executed a real statement yet, so its _query
attribute is still None. poll()'s error path called
get_explain_query_length() on that None unconditionally, crashing with
AttributeError: 'NoneType' object has no attribute 'query' on the next
query error and leaving the Query Tool unusable, instead of returning
the intended JSON error response.
@dpage
dpage force-pushed the fix/8991-servercursor-commit-rollback branch from bd95683 to e70c698 Compare August 25, 2026 09:07
@dpage

dpage commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

@hiteshjambhale Confirmed, thanks for the clear repro. Root cause: poll()'s error path (web/pgadmin/tools/sqleditor/__init__.py, around line 1152) built explain_query_length from conn._Connection__async_cursor._query, guarded only on the cursor itself being truthy, not on _query being set. Once BEGIN/COMMIT/ROLLBACK has run through the throwaway plain cursor this PR introduces, the cached async cursor poll() sees next can be one that hasn't executed a real statement yet, so _query is still None. get_explain_query_length() immediately does query_obj.query.decode(), and with query_obj being None that's the AttributeError: 'NoneType' object has no attribute 'query' you hit - turning any query error after a commit under server cursor mode into an unhandled 500.

Fix: also require _query to be set before calling get_explain_query_length():

'explain_query_length':
get_explain_query_length(conn._Connection__async_cursor._query)
if conn._Connection__async_cursor and
conn._Connection__async_cursor._query else 0

Added a regression test (web/pgadmin/tools/sqleditor/tests/test_poll_explain_query_length_guard.py) that reproduces the crash against unfixed code and passes with the fix. Pushed as a new commit on this branch - could you re-test against your original repro steps (server cursor on, auto commit off, SELECT, commit, then another query) when you get a chance?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Result grid does not move messages tab when commit/rollback button is clicked with server cursor on.

2 participants