From 31a5c5418c28bee74b827a49dccaedb255e8846a Mon Sep 17 00:00:00 2001 From: Dave Page Date: Fri, 16 Oct 2026 01:07:38 +0100 Subject: [PATCH] Keep the Query Tool execute options when its transaction is re-created. #10598 (#10599) --- web/pgadmin/tools/sqleditor/__init__.py | 20 ++- .../js/components/QueryToolComponent.jsx | 19 ++- .../tests/test_execute_options_reinit.py | 118 ++++++++++++++++++ .../sqleditor/QueryToolInitialise.spec.js | 68 ++++++++++ 4 files changed, 220 insertions(+), 5 deletions(-) create mode 100644 web/pgadmin/tools/sqleditor/tests/test_execute_options_reinit.py create mode 100644 web/regression/javascript/sqleditor/QueryToolInitialise.spec.js diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index a605d23a7..54d7658e6 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -452,6 +452,11 @@ def panel(trans_id): params=json.dumps(params)) +# Execute options that can be changed from the Query Tool toolbar, and which +# are kept on the transaction object. +EXECUTE_OPTIONS = ('auto_commit', 'auto_rollback', 'server_cursor') + + @blueprint.route( '/initialize/sqleditor////' '', @@ -496,6 +501,13 @@ def initialize_sqleditor(trans_id, sgid, sid, did=None): 'role': data['role'] if 'role' in data else None, 'password': data['password'] if 'password' in data else None } + # The execute options currently chosen on the toolbar, sent when the + # transaction is re-created, so they are kept rather than reset to the + # preferences. Anything other than a boolean falls back to the + # preference in _init_sqleditor. + for option in EXECUTE_OPTIONS: + if isinstance(data.get(option), bool): + kwargs[option] = data[option] is_error, errmsg, conn_id, version = _init_sqleditor( trans_id, connect, sgid, sid, did, data['dbname'], **kwargs) @@ -721,9 +733,13 @@ def update_sqleditor_connection(trans_id, sgid, sid, did): 'user': data['user'], 'role': data['role'] if 'role' in data else None, 'password': data['password'] if 'password' in data else None, - 'auto_commit': getattr(trans_obj, 'auto_commit', None), - 'auto_rollback': getattr(trans_obj, 'auto_rollback', None), } + # Keep the execute options from the old transaction, but only + # booleans, as initialize_sqleditor() does; anything else falls + # back to the preferences. + for option in EXECUTE_OPTIONS: + value = getattr(trans_obj, option, None) + kwargs[option] = value if isinstance(value, bool) else None is_error, errmsg, conn_id, version = _init_sqleditor( new_trans_id, connect, sgid, sid, did, data['database_name'], diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx index 4cc98eee0..49cb7f5ff 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx @@ -115,6 +115,21 @@ const FIXED_PREF = { }, }; +// The body of the request that initialises the transaction. A Query Tool +// sends the execute options currently chosen on the toolbar, so that they +// are kept rather than reset to the preferences when the transaction is +// re-created; only boolean values are sent, and the server uses the +// preference for any that are missing. +export function getInitialiseRequestData(params, connParams, toolbarOptions) { + if(params.is_query_tool) { + return { + ..._.pickBy(_.pick(toolbarOptions ?? {}, ['auto_commit', 'auto_rollback', 'server_cursor']), _.isBoolean), + ...connParams, + }; + } + return {sql_filter: params.sql_filter, server_cursor: params.server_cursor, ...connParams}; +} + export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedNodeInfo, qtPanelDocker, qtPanelId, eventBusObj}) { const containerRef = React.useRef(null); const preferencesStore = usePreferences(); @@ -397,9 +412,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN password: password, dbname: selectedConn.database_name }; - api.post(baseUrl, qtState.params.is_query_tool ? - {...requestParams} : - {sql_filter: qtState.params.sql_filter, server_cursor: qtState.params.server_cursor, ...requestParams}) + api.post(baseUrl, getInitialiseRequestData(qtState.params, requestParams, toolbarOptionsRef.current)) .then(()=>{ setQtStatePartial({ connected: true, diff --git a/web/pgadmin/tools/sqleditor/tests/test_execute_options_reinit.py b/web/pgadmin/tools/sqleditor/tests/test_execute_options_reinit.py new file mode 100644 index 000000000..5ad0a4eab --- /dev/null +++ b/web/pgadmin/tools/sqleditor/tests/test_execute_options_reinit.py @@ -0,0 +1,118 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2026, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import json +import pickle +import secrets + +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils + +# The preference defaults, which the freshly created test database uses. +PREFERENCE_OPTIONS = { + 'auto_commit': True, + 'auto_rollback': False, + 'server_cursor': False, +} +CHANGED_OPTIONS = { + 'auto_commit': False, + 'auto_rollback': True, + 'server_cursor': True, +} + + +class TestExecuteOptionsReinit(BaseTestGenerator): + """ + The execute options chosen on the Query Tool toolbar must be kept when + the transaction is re-created, rather than reset to the preferences + (issue #10598). + """ + scenarios = [ + ('Initialise with the toolbar execute options', dict( + change_connection=False, init_options=CHANGED_OPTIONS, + expected=CHANGED_OPTIONS)), + ('Initialise with some of the toolbar execute options', dict( + change_connection=False, + init_options={'server_cursor': True}, + expected=dict(PREFERENCE_OPTIONS, server_cursor=True))), + ('Initialise without execute options uses the preferences', dict( + change_connection=False, init_options={}, + expected=PREFERENCE_OPTIONS)), + ('Initialise ignores execute options that are not booleans', dict( + change_connection=False, + init_options={'auto_commit': 'false', 'auto_rollback': None, + 'server_cursor': 1}, + expected=PREFERENCE_OPTIONS)), + ('Changing the connection keeps the execute options', dict( + change_connection=True, init_options={}, + toolbar_options=CHANGED_OPTIONS, expected=CHANGED_OPTIONS)), + ('Changing the connection ignores execute options that are not ' + 'booleans', dict( + change_connection=True, init_options={}, + toolbar_options={'auto_commit': 'false', 'auto_rollback': 1, + 'server_cursor': 1}, + expected=PREFERENCE_OPTIONS)), + ] + + def setUp(self): + database_info = parent_node_dict["database"][-1] + self.db_name = database_info["db_name"] + self.server_id = database_info["server_id"] + self.db_id = database_info["db_id"] + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if not db_con["info"] == "Database connected.": + raise Exception("Could not connect to the database.") + + self.trans_id = str(secrets.choice(range(1, 9999999))) + url = '/sqleditor/initialize/sqleditor/{0}/{1}/{2}/{3}'.format( + self.trans_id, utils.SERVER_GROUP, self.server_id, self.db_id) + response = self.tester.post(url, data=json.dumps(dict( + self.init_options, dbname=self.db_name))) + self.assertEqual(response.status_code, 200) + + def runTest(self): + if self.change_connection: + # Change the options from the toolbar, then change the + # connection, which creates a new transaction. + for option, value in self.toolbar_options.items(): + response = self.tester.post( + '/sqleditor/{0}/{1}'.format(option, self.trans_id), + data=json.dumps(value)) + self.assertEqual(response.status_code, 200) + + url = ('/sqleditor/initialize/sqleditor/update_connection/' + '{0}/{1}/{2}/{3}').format(self.trans_id, + utils.SERVER_GROUP, + self.server_id, self.db_id) + response = self.tester.post(url, data=json.dumps({ + 'user': self.server['username'], + 'role': None, + 'password': self.server['db_password'], + 'database_name': self.db_name, + }), content_type='html/json') + self.assertEqual(response.status_code, 200) + self.trans_id = json.loads( + response.data.decode('utf-8'))['data']['trans_id'] + + with self.tester.session_transaction() as sess: + command_obj = pickle.loads( + sess['gridData'][str(self.trans_id)]['command_obj']) + + for option, value in self.expected.items(): + self.assertIs(getattr(command_obj, option), value, option) + + def tearDown(self): + self.tester.delete('/sqleditor/close/{0}'.format(self.trans_id)) + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/regression/javascript/sqleditor/QueryToolInitialise.spec.js b/web/regression/javascript/sqleditor/QueryToolInitialise.spec.js new file mode 100644 index 000000000..b732c6d8e --- /dev/null +++ b/web/regression/javascript/sqleditor/QueryToolInitialise.spec.js @@ -0,0 +1,68 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2026, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +// QueryToolComponent.jsx pulls in editors and viewers that this test never +// renders, and whose ESM-only dependencies Jest cannot load. +jest.mock('../../../pgadmin/static/js/components/ReactCodeMirror', () => ({ + __esModule: true, + default: ({ value }) => value, +})); +jest.mock('../../../pgadmin/static/js/components/JsonEditor', () => ({ + __esModule: true, + default: () => null, +})); +jest.mock('../../../pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer', () => ({ + GeometryViewer: () => null, +})); +jest.mock('../../../pgadmin/tools/sqleditor/static/js/components/sections/GraphVisualiser', () => ({ + GraphVisualiser: () => null, +})); + +import { getInitialiseRequestData } from '../../../pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx'; + +const connParams = { + user: 'postgres', + role: null, + password: '', + dbname: 'postgres', +}; + +describe('getInitialiseRequestData', () => { + it('sends the toolbar execute options for a Query Tool', () => { + expect(getInitialiseRequestData({is_query_tool: true}, connParams, { + auto_commit: false, + auto_rollback: true, + server_cursor: true, + explain_verbose: true, + open_in_new_tab: false, + })).toEqual({ + ...connParams, + auto_commit: false, + auto_rollback: true, + server_cursor: true, + }); + }); + + it('leaves out execute options that are not known yet', () => { + expect(getInitialiseRequestData({is_query_tool: true}, connParams, null)) + .toEqual(connParams); + expect(getInitialiseRequestData({is_query_tool: true}, connParams, {})) + .toEqual(connParams); + expect(getInitialiseRequestData({is_query_tool: true}, connParams, { + auto_commit: undefined, server_cursor: false, + })).toEqual({...connParams, server_cursor: false}); + }); + + it('does not send the toolbar execute options for View/Edit Data', () => { + expect(getInitialiseRequestData({ + is_query_tool: false, sql_filter: 'id > 1', server_cursor: true, + }, connParams, {auto_commit: false, auto_rollback: true, server_cursor: false})) + .toEqual({...connParams, sql_filter: 'id > 1', server_cursor: true}); + }); +});