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

Fix the Query Tool not re-running the query after reconnecting at a password prompt. #10603 (#10605)

This commit is contained in:
Dave Page
2026-10-15 18:02:30 +01:00
parent 7828c9b2a1
commit 8d8bcf3748
2 changed files with 175 additions and 2 deletions

View File

@@ -253,10 +253,20 @@ export class ResultSetUtils {
}
}
} catch(e) {
/* After reconnecting, re-initialise the transaction and re-run the
* query. REINIT_QT_CONNECTION is handled by initializeQueryTool(password,
* explainObject, macroSQL, executeCursor, executeServerCursor, reexecute):
* executeServerCursor only resets the status bar's server cursor text
* (queryFinished sets it again if the re-run uses one), so it is false,
* and reexecute must be true or the Query Tool does not re-run. #10603
* The re-run rebuilds the query from the editor (or from macroSQL), so
* an external query without a macro, such as the toolbar's COMMIT or
* ROLLBACK, is not re-run: the editor's contents would run instead. */
const reexecute = !(flags.external && _.isEmpty(macroSQL));
if(e?.response?.status == 428){
connectServerModal(this.queryToolCtx.modal, e.response?.data?.result, async (passwordData)=>{
await connectServer(this.api, this.queryToolCtx.modal, this.queryToolCtx.params.sid, this.queryToolCtx.params.user, passwordData, async ()=>{
await this.eventBus.fireEvent(QUERY_TOOL_EVENTS.REINIT_QT_CONNECTION, '', explainObject, macroSQL, flags.executeCursor, true);
await this.eventBus.fireEvent(QUERY_TOOL_EVENTS.REINIT_QT_CONNECTION, '', explainObject, macroSQL, flags.executeCursor, false, reexecute);
});
}, ()=>{
/*This is intentional (SonarQube)*/
@@ -265,7 +275,7 @@ export class ResultSetUtils {
let pgBrowser = window.pgAdmin.Browser;
pgBrowser.set_master_password('', async (passwordData)=>{
await connectServer(this.api, this.queryToolCtx.modal, this.queryToolCtx.params.sid, this.queryToolCtx.params.user, passwordData, async ()=>{
await this.eventBus.fireEvent(QUERY_TOOL_EVENTS.REINIT_QT_CONNECTION, '', explainObject, macroSQL, flags.executeCursor, true);
await this.eventBus.fireEvent(QUERY_TOOL_EVENTS.REINIT_QT_CONNECTION, '', explainObject, macroSQL, flags.executeCursor, false, reexecute);
});
}, ()=> {
/*This is intentional (SonarQube)*/

View File

@@ -0,0 +1,163 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
// When a query needs the server password (HTTP 428) or the master password
// crypt key is missing, the user is prompted and the connection is
// re-initialised through REINIT_QT_CONNECTION, which QueryToolComponent hands
// to initializeQueryTool(password, explainObject, macroSQL, executeCursor,
// executeServerCursor, reexecute). reexecute must be true, or the Query Tool
// does not re-run the query, and executeServerCursor must be false. #10603
// The re-run rebuilds the query from the editor or macroSQL, so an external
// query with no macro (the toolbar's COMMIT or ROLLBACK) is not re-run.
jest.mock('sources/url_for', () => ({
__esModule: true,
default: jest.fn((endpoint) => `/mock/${endpoint}`),
}));
jest.mock('../../../pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx', () => {
const React = require('react');
return {
QueryToolContext: React.createContext(null),
QueryToolConnectionContext: React.createContext(null),
QueryToolEventsContext: React.createContext(null),
};
});
jest.mock('../../../pgadmin/static/js/components/ReactCodeMirror', () => ({
__esModule: true,
default: ({ value }) => value,
}));
jest.mock('../../../pgadmin/static/js/components/JsonEditor', () => ({
__esModule: true,
default: () => null,
}));
// ResultSet.jsx pulls in the map and chart viewers, which this test never
// renders and whose ESM-only dependencies Jest cannot load.
jest.mock('../../../pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer', () => ({
GeometryViewer: () => null,
}));
jest.mock('../../../pgadmin/tools/sqleditor/static/js/components/sections/GraphVisualiser', () => ({
GraphVisualiser: () => null,
}));
// Accept the password prompt and the reconnection straight away.
jest.mock('../../../pgadmin/tools/sqleditor/static/js/components/connectServer', () => ({
connectServerModal: jest.fn((_modal, _data, connectCallback) => connectCallback({password: 'secret'})),
connectServer: jest.fn(async (_api, _modal, _sid, _user, _formData, connectCallback) => connectCallback()),
}));
import { ResultSetUtils } from '../../../pgadmin/tools/sqleditor/static/js/components/sections/ResultSet';
import { QUERY_TOOL_EVENTS } from '../../../pgadmin/tools/sqleditor/static/js/components/QueryToolConstants';
describe('ResultSetUtils.startExecution reconnection', () => {
const explainObject = {analyze: false};
const macroSQL = 'SELECT $SELECTION$';
let eventBus, api, rsu, originalPgAdmin;
beforeEach(() => {
originalPgAdmin = window.pgAdmin;
eventBus = {
fireEvent: jest.fn(),
registerListener: jest.fn(),
deregisterListener: jest.fn(),
};
api = {post: jest.fn(), get: jest.fn()};
rsu = new ResultSetUtils(api, {modal: {}, params: {sid: 1, user: 'postgres'}}, 1234, true);
rsu.setEventBus(eventBus);
});
afterEach(() => {
window.pgAdmin = originalPgAdmin;
});
const reinitCalls = () => eventBus.fireEvent.mock.calls.filter(
(c) => c[0] === QUERY_TOOL_EVENTS.REINIT_QT_CONNECTION
);
// Name the arguments as initializeQueryTool receives them.
const reinitArgs = (call) => {
const [, password, explain, macro, executeCursor, executeServerCursor, reexecute] = call;
return {password, explain, macro, executeCursor, executeServerCursor, reexecute};
};
// The prompt callbacks are not awaited by startExecution, so let them finish.
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));
const expectRerun = async () => {
await flushPromises();
const calls = reinitCalls();
expect(calls).toHaveLength(1);
expect(reinitArgs(calls[0])).toEqual({
password: '',
explain: explainObject,
macro: macroSQL,
executeCursor: true,
executeServerCursor: false,
reexecute: true,
});
};
const flags = {isQueryTool: true, external: false, reconnect: false, executeCursor: true, refreshData: false};
it('re-runs the query after the server password prompt', async () => {
api.post.mockRejectedValue({response: {status: 428, data: {result: {}}}});
await rsu.startExecution('SELECT 1', explainObject, macroSQL, jest.fn(), flags);
await expectRerun();
});
it('re-runs the query after the master password prompt', async () => {
window.pgAdmin = {Browser: {set_master_password: jest.fn((_p, cb) => cb({password: 'secret'}))}};
api.post.mockRejectedValue({response: {status: 500, data: {info: 'CRYPTKEY_MISSING'}}});
await rsu.startExecution('SELECT 1', explainObject, macroSQL, jest.fn(), flags);
await expectRerun();
});
// The toolbar's COMMIT and ROLLBACK run external queries with no macro, and
// a re-run would rebuild the query from the editor rather than run them.
const externalFlags = {...flags, external: true, executeCursor: false};
const expectNoRerun = async () => {
await flushPromises();
const calls = reinitCalls();
expect(calls).toHaveLength(1);
expect(reinitArgs(calls[0]).reexecute).toBe(false);
};
it('does not re-run an external query without a macro after the server password prompt', async () => {
api.post.mockRejectedValue({response: {status: 428, data: {result: {}}}});
await rsu.startExecution('COMMIT;', null, undefined, jest.fn(), externalFlags);
await expectNoRerun();
});
it('does not re-run an external query without a macro after the master password prompt', async () => {
window.pgAdmin = {Browser: {set_master_password: jest.fn((_p, cb) => cb({password: 'secret'}))}};
api.post.mockRejectedValue({response: {status: 500, data: {info: 'CRYPTKEY_MISSING'}}});
await rsu.startExecution('ROLLBACK;', null, undefined, jest.fn(), externalFlags);
await expectNoRerun();
});
it('re-runs an external macro query, which rebuilds from macroSQL', async () => {
api.post.mockRejectedValue({response: {status: 428, data: {result: {}}}});
await rsu.startExecution('SELECT 1', explainObject, macroSQL, jest.fn(), {...flags, external: true});
await expectRerun();
});
});