-
Notifications
You must be signed in to change notification settings - Fork 809
Warn user of unsaved data output edits and optionally allow to save data before page navigation. #8916 #9521
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
…ata before page navigation. pgadmin-org#8916
WalkthroughThese 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
Sequence DiagramssequenceDiagram
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this 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 thattriggerSaveDatais stable.Since
triggerSaveDatais wrapped withuseLatestFunc, 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
[]sincetriggerSaveDatareference is guaranteed stable.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
web/pgadmin/static/js/custom_hooks.jsweb/pgadmin/static/js/helpers/ModalProvider.jsxweb/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.jsweb/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsxweb/pgadmin/tools/sqleditor/utils/start_running_query.pyweb/pgadmin/utils/driver/psycopg3/connection.pyweb/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
closeModaldirectly to the cancel button (line 94)- Wiring the
onCancelClickcallback through the modal'sonClosehandler (line 96)This ensures
onCancelClickis 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_loopmethod 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_busymethod 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_executecorrectly routes async cursor execution through the dedicated event loop usingrun_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:
- Initialize the event loop before async operations (line 1109)
- Check if the connection is busy to prevent concurrent queries (lines 1145-1148)
- 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_asyncmethod correctly:
- Schedules connection closure on the event loop
- Stops the loop gracefully with
call_soon_threadsafe- Joins the thread with a timeout to avoid indefinite blocking
- 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_ENDevent 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 inexecuteis intentional and properly handled by all call sites.The
executemethod returns a coroutine directly (line 280) while methods likefetchmany,fetchall,fetchone, andclose_cursorwrap async operations withasyncio.run. This is an intentional design choice:executeis called from within async functions that are managed byrun_coroutine_threadsafe()in__internal_blocking_execute()(line 859, 1153), which properly awaits the coroutine. The other methods useasyncio.run()because they bridge from sync to async code. All call sites in connection.py correctly handle the coroutine return type viaawaitwithin 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
useEffectprovides 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_ENDevent listener, ensuring data is persisted before moving to the next page.
1203-1305: LGTM! Well-structured save flow with proper event coordination.Wrapping
triggerSaveDatawithuseLatestFuncensures the callback always operates on current state values. TheSAVE_DATA_ENDevent emissions correctly signal success/failure to waiting listeners.
| 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)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Address static analysis findings in async execution error handling.
Three issues flagged by static analysis:
-
Unused variable (line 165): The
statusreturn value fromexecute_asyncis captured but never used. Either use it or discard explicitly with_. -
Overly broad exception catch (line 172): Catching bare
Exceptionis too permissive. Consider catching specific exceptions (e.g.,psycopg.Error,ConnectionLost) to avoid masking unexpected errors. -
Suboptimal logging (line 173): Use
self.logger.exception(...)instead ofself.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.
Summary by CodeRabbit
Bug Fixes
New Features
✏️ Tip: You can customize this high-level summary in your review settings.