1
0
mirror of synced 2026-10-23 07:08:55 +03:00

Keep the Query Tool execute options when its transaction is re-created. #10598 (#10599)

This commit is contained in:
Dave Page
2026-10-16 01:07:38 +01:00
parent cc1e1ef184
commit 31a5c5418c
4 changed files with 220 additions and 5 deletions

View File

@@ -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/<int:trans_id>/<int:sgid>/<int:sid>/'
'<did>',
@@ -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'],

View File

@@ -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,

View File

@@ -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)

View File

@@ -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});
});
});