Skip to content

Conversation

@adityatoshniwal
Copy link
Contributor

@adityatoshniwal adityatoshniwal commented Jan 8, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of unsaved changes in the SQL editor with clearer save/discard prompts
    • Fixed data synchronization issues when query results become out-of-sync
    • Enhanced cancel button behavior in confirmation modals
  • New Features

    • Added automatic data refresh when results go out-of-sync
    • Improved async query execution stability with enhanced background processing

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 8, 2026

Walkthrough

These changes implement a threaded event loop for asynchronous psycopg3 operations, add data synchronization tracking to the SQL editor's result set component, refactor modal callback handling for consistency, and introduce a utility hook for stable callback references. The backend async architecture now manages a dedicated event loop in a background thread to handle concurrent query execution.

Changes

Cohort / File(s) Summary
Frontend Hook & Event Utilities
web/pgadmin/static/js/custom_hooks.js, web/pgadmin/tools/sqleditor/static/js/components/sections/QueryToolConstants.js
Added useLatestFunc hook for stable callback references; introduced SAVE_DATA_END event constant to signify completion of data save operations.
Frontend Modal & Component Logic
web/pgadmin/static/js/helpers/ModalProvider.jsx, web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx
Changed cancel click handler to close modal directly; refactored ResultSet to track data sync state with refs, integrate save-data modals, handle listener cleanup, and conditionally re-fetch data after execution cycles.
Backend Async Architecture
web/pgadmin/utils/driver/psycopg3/connection.py, web/pgadmin/utils/driver/psycopg3/cursor.py
Implemented threaded event loop per Connection instance to manage async psycopg3 operations; simplified AsyncDictCursor.execute to delegate event loop management to caller; added is_busy() method to expose query execution state.
Backend Query Execution
web/pgadmin/tools/sqleditor/utils/start_running_query.py
Changed status initialization from -1 to True; captured execute_async result for status handling; removed native thread ID assignment; expanded error logging for background execution failures.

Sequence Diagrams

sequenceDiagram
    participant Caller
    participant Connection
    participant EventLoop
    participant Thread as Background Thread
    participant Cursor
    participant DB as Database
    
    Caller->>Connection: execute_async(query)
    activate Connection
    Connection->>Connection: _start_event_loop()
    activate EventLoop
    EventLoop->>Thread: Create & start thread with loop
    activate Thread
    Thread->>Thread: asyncio.run(loop)
    deactivate Thread
    deactivate EventLoop
    
    Connection->>EventLoop: run_coroutine_threadsafe(async_execute)
    activate EventLoop
    EventLoop->>Cursor: _execute(query)
    activate Cursor
    Cursor->>DB: execute
    activate DB
    DB-->>Cursor: result
    deactivate DB
    Cursor-->>EventLoop: result
    deactivate Cursor
    EventLoop-->>Connection: result
    deactivate EventLoop
    
    Connection-->>Caller: status, result
    deactivate Connection
Loading
sequenceDiagram
    participant ResultSet
    participant Modal
    participant DataStore as DataChangeStore
    participant API
    participant Listener as Event Listener
    
    ResultSet->>DataStore: Check isDataChangedRef.current
    alt Data Changed
        ResultSet->>Modal: Show save confirmation modal
        activate Modal
        Modal->>ResultSet: User clicks Save or Discard
        deactivate Modal
        alt Save
            ResultSet->>API: triggerSaveData()
            activate API
            API->>DataStore: Save data to server
            API-->>ResultSet: SAVE_DATA_END event
            deactivate API
            ResultSet->>Listener: Register SAVE_DATA_END handler
            activate Listener
            Listener->>ResultSet: On event, reset state & re-fetch
            deactivate Listener
        else Discard
            ResultSet->>ResultSet: Mark as out-of-sync, reset state
        end
    else No Changes
        ResultSet->>ResultSet: Proceed with fetch
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the changeset - warning users about unsaved data edits and providing an option to save before navigation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @web/pgadmin/tools/sqleditor/utils/start_running_query.py:
- Around line 165-174: The code captures an unused return value from
conn.execute_async into status, catches a broad Exception, and logs without
traceback; change the assignment to discard the unused value (use _ =
conn.execute_async(...) or drop assignment), narrow the except to specific
DB/connection exceptions (e.g., psycopg.Error, ConnectionLost or your project’s
DBError types) so unexpected exceptions propagate, and replace
self.logger.error(...) with self.logger.exception(...) to include tracebacks;
keep the existing rollback logic using is_rollback_req and still return
internal_server_error(errormsg=str(e)) for handled DB/connection errors.
🧹 Nitpick comments (3)
web/pgadmin/utils/driver/psycopg3/connection.py (1)

1089-1092: Refine exception handling in async cursor cleanup.

Catching bare Exception (line 1091) is overly broad and could mask unexpected errors during cursor cleanup. Consider catching specific psycopg exceptions or asyncio-related errors, and using proper logging instead of print statements.

♻️ Proposed fix
     def release_async_cursor(self):
         if self.__async_cursor and not self.__async_cursor.closed:
             try:
                 run_coroutine_threadsafe(self.__async_cursor.close_cursor(),
                                          self._loop).result()
-            except Exception as e:
-                print("Exception==", str(e))
+            except (psycopg.Error, asyncio.CancelledError) as e:
+                current_app.logger.warning(
+                    f"Failed to close async cursor for connection {self.conn_id}: {e}"
+                )
web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx (2)

1133-1134: Remove commented-out code.

Line 1134 contains dead code that should be removed to keep the codebase clean.

🧹 Remove dead code
          eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, {refreshData: true});
-          // executionStartCallback(rsu.current.query, {refreshData: true});
        } else {

1307-1310: Consider simplifying dependencies now that triggerSaveData is stable.

Since triggerSaveData is wrapped with useLatestFunc, it maintains a stable reference while always invoking the latest function. The dependencies [dataChangeStore, rows, columns] are now unnecessary and cause redundant listener re-registration on each change.

♻️ Simplify effect dependencies
 useEffect(()=>{
   eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_SAVE_DATA, triggerSaveData);
   return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_SAVE_DATA, triggerSaveData);
-}, [dataChangeStore, rows, columns]);
+}, [triggerSaveData]);

Or simply [] since triggerSaveData reference is guaranteed stable.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 62e2d18 and 5a99c80.

📒 Files selected for processing (7)
  • web/pgadmin/static/js/custom_hooks.js
  • web/pgadmin/static/js/helpers/ModalProvider.jsx
  • web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js
  • web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx
  • web/pgadmin/tools/sqleditor/utils/start_running_query.py
  • web/pgadmin/utils/driver/psycopg3/connection.py
  • web/pgadmin/utils/driver/psycopg3/cursor.py
🧰 Additional context used
🧬 Code graph analysis (4)
web/pgadmin/utils/driver/psycopg3/cursor.py (1)
web/pgadmin/utils/driver/psycopg3/connection.py (1)
  • _execute (509-516)
web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx (2)
web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js (4)
  • QUERY_TOOL_EVENTS (12-87)
  • QUERY_TOOL_EVENTS (12-87)
  • CONNECTION_STATUS (89-95)
  • CONNECTION_STATUS (89-95)
web/pgadmin/static/js/custom_hooks.js (1)
  • useLatestFunc (290-295)
web/pgadmin/utils/driver/psycopg3/connection.py (2)
web/pgadmin/utils/driver/psycopg3/typecast.py (1)
  • register_global_typecasters (131-158)
web/pgadmin/utils/driver/psycopg3/cursor.py (2)
  • close_cursor (210-214)
  • close_cursor (305-309)
web/pgadmin/static/js/helpers/ModalProvider.jsx (3)
web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx (1)
  • modalId (847-847)
web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx (1)
  • modalId (69-69)
web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx (1)
  • modalId (59-59)
🪛 Ruff (0.14.10)
web/pgadmin/tools/sqleditor/utils/start_running_query.py

165-165: Unpacked variable status is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


172-172: Do not catch blind exception: Exception

(BLE001)


173-173: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

web/pgadmin/utils/driver/psycopg3/connection.py

1091-1091: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: run-python-tests-pg (windows-latest, 18)
  • GitHub Check: run-python-tests-pg (windows-latest, 15)
  • GitHub Check: run-python-tests-pg (windows-latest, 13)
  • GitHub Check: run-python-tests-pg (macos-latest, 18)
  • GitHub Check: run-python-tests-pg (macos-latest, 16)
  • GitHub Check: run-feature-tests-pg (16)
  • GitHub Check: run-feature-tests-pg (17)
  • GitHub Check: run-feature-tests-pg (18)
  • GitHub Check: run-feature-tests-pg (15)
  • GitHub Check: run-feature-tests-pg (14)
  • GitHub Check: run-feature-tests-pg (13)
🔇 Additional comments (13)
web/pgadmin/static/js/helpers/ModalProvider.jsx (1)

86-97: LGTM! Modal callback wiring refactored for consistency.

The refactoring centralizes modal close handling by:

  • Passing closeModal directly to the cancel button (line 94)
  • Wiring the onCancelClick callback through the modal's onClose handler (line 96)

This ensures onCancelClick is invoked consistently across all close scenarios (cancel button, backdrop click, escape key), improving the reliability of cleanup logic.

web/pgadmin/utils/driver/psycopg3/connection.py (5)

199-212: LGTM! Threaded event loop implementation enables async psycopg3 in Flask.

The _start_event_loop method creates a dedicated asyncio event loop in a background daemon thread, which is the correct approach for integrating async operations within Flask's synchronous request-handling model. The thread is properly named for debugging, and the alive check prevents creating duplicate loops.


287-292: LGTM! Efficient busy state detection for async connections.

The is_busy method correctly checks the connection's internal lock state to determine if a query is executing, avoiding polling overhead. This provides a lightweight way to prevent concurrent query execution on the same connection.


855-861: LGTM! Proper async execution routing through the event loop.

The refactored __internal_blocking_execute correctly routes async cursor execution through the dedicated event loop using run_coroutine_threadsafe(...).result(), making it a blocking call from the caller's perspective while allowing the async psycopg3 cursor to execute on the background loop.


1109-1153: LGTM! Proper async execution with busy check and event loop coordination.

The changes correctly:

  1. Initialize the event loop before async operations (line 1109)
  2. Check if the connection is busy to prevent concurrent queries (lines 1145-1148)
  3. Execute the query on the dedicated event loop using run_coroutine_threadsafe (lines 1150-1153)

This ensures safe concurrent execution and proper error handling.


1548-1560: LGTM! Proper async connection cleanup with loop shutdown.

The _close_async method correctly:

  1. Schedules connection closure on the event loop
  2. Stops the loop gracefully with call_soon_threadsafe
  3. Joins the thread with a timeout to avoid indefinite blocking
  4. Cleans up references to prevent resource leaks

The 1-second timeout on thread join is reasonable for cleanup operations.

web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js (1)

52-52: LGTM! New event constant added for data save completion.

The SAVE_DATA_END event constant follows the existing naming convention and is appropriately placed with related event definitions. This supports the broader data synchronization tracking feature mentioned in the PR objectives.

web/pgadmin/utils/driver/psycopg3/cursor.py (1)

280-280: Async pattern in execute is intentional and properly handled by all call sites.

The execute method returns a coroutine directly (line 280) while methods like fetchmany, fetchall, fetchone, and close_cursor wrap async operations with asyncio.run. This is an intentional design choice: execute is called from within async functions that are managed by run_coroutine_threadsafe() in __internal_blocking_execute() (line 859, 1153), which properly awaits the coroutine. The other methods use asyncio.run() because they bridge from sync to async code. All call sites in connection.py correctly handle the coroutine return type via await within async contexts.

web/pgadmin/static/js/custom_hooks.js (1)

290-295: LGTM! Well-implemented stable callback pattern.

This hook correctly implements the "latest ref" pattern for creating stable callbacks that always invoke the most recent function reference. Minor style note: the outer parentheses around the arrow function on line 294 are unnecessary.

return useCallback((...args) => fnRef.current(...args), []);
web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx (4)

877-880: LGTM! Clean approach for tracking data changes.

Using a ref synchronized via useEffect provides a reliable way to check for unsaved changes without stale closure issues in callbacks.


1093-1120: LGTM! Proper state reset before fetching new data.

Adding resetSelectionAndChanges() ensures clean slate when navigating to a different data window.


1145-1160: Save confirmation modal flow looks correct.

The implementation properly coordinates save completion with page navigation via the SAVE_DATA_END event listener, ensuring data is persisted before moving to the next page.


1203-1305: LGTM! Well-structured save flow with proper event coordination.

Wrapping triggerSaveData with useLatestFunc ensures the callback always operates on current state values. The SAVE_DATA_END event emissions correctly signal success/failure to waiting listeners.

Comment on lines +165 to 174
status, _ = conn.execute_async(
sql, server_cursor=trans_obj.server_cursor)
# If the transaction aborted for some reason and
# Auto RollBack is True then issue a rollback
# to cleanup.
# If the transaction aborted for some reason and
# Auto RollBack is True then issue a rollback
# to cleanup.
if is_rollback_req:
conn.execute_void("ROLLBACK;")
except Exception as e:
self.logger.error(e)
self.logger.error(f"Error in background execution: {e}")
return internal_server_error(errormsg=str(e))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Address static analysis findings in async execution error handling.

Three issues flagged by static analysis:

  1. Unused variable (line 165): The status return value from execute_async is captured but never used. Either use it or discard explicitly with _.

  2. Overly broad exception catch (line 172): Catching bare Exception is too permissive. Consider catching specific exceptions (e.g., psycopg.Error, ConnectionLost) to avoid masking unexpected errors.

  3. Suboptimal logging (line 173): Use self.logger.exception(...) instead of self.logger.error(...) in exception handlers to automatically include the traceback.

🔧 Proposed fix
                     else:
-                        status, _ = conn.execute_async(
+                        _, _ = conn.execute_async(
                             sql, server_cursor=trans_obj.server_cursor)
                     # If the transaction aborted for some reason and
                     # Auto RollBack is True then issue a rollback
                     # to cleanup.
                     if is_rollback_req:
                         conn.execute_void("ROLLBACK;")
-                except Exception as e:
-                    self.logger.error(f"Error in background execution: {e}")
+                except (psycopg.Error, ConnectionLost) as e:
+                    self.logger.exception("Error in background execution")
                     return internal_server_error(errormsg=str(e))

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.10)

165-165: Unpacked variable status is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


172-172: Do not catch blind exception: Exception

(BLE001)


173-173: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🤖 Prompt for AI Agents
In @web/pgadmin/tools/sqleditor/utils/start_running_query.py around lines 165 -
174, The code captures an unused return value from conn.execute_async into
status, catches a broad Exception, and logs without traceback; change the
assignment to discard the unused value (use _ = conn.execute_async(...) or drop
assignment), narrow the except to specific DB/connection exceptions (e.g.,
psycopg.Error, ConnectionLost or your project’s DBError types) so unexpected
exceptions propagate, and replace self.logger.error(...) with
self.logger.exception(...) to include tracebacks; keep the existing rollback
logic using is_rollback_req and still return
internal_server_error(errormsg=str(e)) for handled DB/connection errors.

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.

1 participant