diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/test_utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/test_utils.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/static/js/sqleditor/execute_query.js b/web/pgadmin/static/js/sqleditor/execute_query.js new file mode 100644 index 00000000..e91c9e85 --- /dev/null +++ b/web/pgadmin/static/js/sqleditor/execute_query.js @@ -0,0 +1,287 @@ +////////////////////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////////////////// + +import gettext from '../gettext'; +import $ from 'jquery'; +import url_for from '../url_for'; +import axios from 'axios'; +import * as transaction from './is_new_transaction_required'; + +class LoadingScreen { + constructor(sqlEditor) { + this.sqlEditor = sqlEditor; + } + + setMessage(message) { + this.sqlEditor.trigger( + 'pgadmin-sqleditor:loading-icon:message', + gettext(message) + ); + } + + show(withMessage) { + this.sqlEditor.trigger( + 'pgadmin-sqleditor:loading-icon:show', + withMessage + ); + } + + hide() { + this.sqlEditor.trigger('pgadmin-sqleditor:loading-icon:hide'); + } +} + +class ExecuteQuery { + constructor(sqlEditor, userManagement) { + this.sqlServerObject = sqlEditor; + this.loadingScreen = new LoadingScreen(sqlEditor); + this.userManagement = userManagement; + } + + delayedPoll() { + const self = this; + setTimeout( + () => { + self.poll(); + }, self.sqlServerObject.POLL_FALLBACK_TIME()); + } + + execute(sqlStatement, explainPlan) { + // If it is an empty query, do nothing. + if (sqlStatement.length <= 0) return; + + const self = this; + let service = axios.create({}); + self.explainPlan = explainPlan; + + const sqlStatementWithAnalyze = ExecuteQuery.prepareAnalyzeSql(sqlStatement, explainPlan); + + self.initializeExecutionOnSqlEditor(sqlStatementWithAnalyze); + + service.post( + url_for('sqleditor.query_tool_start', { + 'trans_id': self.sqlServerObject.transId, + }), + JSON.stringify(sqlStatementWithAnalyze), + {headers: {'Content-Type': 'application/json'}}) + .then(function (result) { + let httpMessageData = result.data; + self.removeGridViewMarker(); + + if (ExecuteQuery.isSqlCorrect(httpMessageData)) { + self.loadingScreen.setMessage('Waiting for the query execution to complete...'); + + self.updateSqlEditorStateWithInformationFromServer(httpMessageData.data); + + // If status is True then poll the result. + self.delayedPoll(); + } else { + self.loadingScreen.hide(); + self.enableSQLEditorButtons(); + self.sqlServerObject.update_msg_history(false, httpMessageData.data.result); + + // Highlight the error in the sql panel + self.sqlServerObject._highlight_error(httpMessageData.data.result); + } + }).catch(function (error) { + self.onExecuteHTTPError(error.response.data); + } + ); + } + + poll() { + const self = this; + let service = axios.create({}); + service.get( + url_for('sqleditor.poll', { + 'trans_id': self.sqlServerObject.transId, + }) + ).then( + (httpMessage) => { + if (ExecuteQuery.isQueryFinished(httpMessage)) { + self.loadingScreen.setMessage('Loading data from the database server and rendering...'); + + self.sqlServerObject.call_render_after_poll(httpMessage.data.data); + } else if (ExecuteQuery.isQueryStillRunning(httpMessage)) { + // If status is Busy then poll the result by recursive call to the poll function + this.delayedPoll(); + self.sqlServerObject.setIsQueryRunning(true); + if (httpMessage.data.data.result) { + self.sqlServerObject.update_msg_history(httpMessage.data.data.status, httpMessage.data.data.result, false); + } + } else if (ExecuteQuery.isConnectionToServerLostWhilePolling(httpMessage)) { + self.loadingScreen.hide(); + // Enable/Disable query tool button only if is_query_tool is true. + if (self.sqlServerObject.is_query_tool) { + self.enableSQLEditorButtons(); + } + self.sqlServerObject.update_msg_history(false, httpMessage.data.data.result, true); + } else if (ExecuteQuery.isQueryCancelled(httpMessage)) { + self.loadingScreen.hide(); + self.sqlServerObject.update_msg_history(false, 'Execution Cancelled!', true); + } + } + ).catch( + error => { + const errorData = error.response.data; + // Enable/Disable query tool button only if is_query_tool is true. + self.sqlServerObject.resetQueryHistoryObject(self.sqlServerObject); + self.loadingScreen.hide(); + if (self.sqlServerObject.is_query_tool) { + self.enableSQLEditorButtons(); + } + + if (ExecuteQuery.wasConnectionLostToServer(errorData)) { + self.handleConnectionToServerLost(); + return; + } + if (self.userManagement.is_pga_login_required(errorData)) { + return self.userManagement.pga_login(); + } + + let msg = ExecuteQuery.extractErrorMessage(errorData); + + self.sqlServerObject.update_msg_history(false, msg); + // Highlight the error in the sql panel + self.sqlServerObject._highlight_error(msg); + }); + } + + initializeExecutionOnSqlEditor(sqlStatement) { + this.loadingScreen.show('Initializing query execution...'); + + $('#btn-flash').prop('disabled', true); + + this.sqlServerObject.query_start_time = new Date(); + if(typeof sqlStatement === 'object') { + this.sqlServerObject.query = sqlStatement['sql']; + } else { + this.sqlServerObject.query = sqlStatement; + } + + this.sqlServerObject.rows_affected = 0; + this.sqlServerObject._init_polling_flags(); + this.disableSQLEditorButtons(); + } + + static prepareAnalyzeSql(sqlStatement, analyzeSql) { + let sqlStatementWithAnalyze = { + sql: sqlStatement, + explain_plan: analyzeSql, + }; + return sqlStatementWithAnalyze; + } + + onExecuteHTTPError(httpMessage) { + this.loadingScreen.hide(); + this.enableSQLEditorButtons(); + + if (ExecuteQuery.wasConnectionLostToServer(httpMessage)) { + this.handleConnectionToServerLost(); + return; + } + + if (this.userManagement.is_pga_login_required(httpMessage)) { + this.sqlServerObject.save_state('execute', [this.explainPlan]); + this.userManagement.pga_login(); + } + + if (transaction.is_new_transaction_required(httpMessage)) { + this.sqlServerObject.save_state('execute', [this.explainPlan]); + this.sqlServerObject.init_transaction(); + } + + let msg = httpMessage.errormsg; + if (httpMessage.responseJSON !== undefined) { + if (httpMessage.responseJSON.errormsg !== undefined) { + msg = httpMessage.responseJSON.errormsg; + } + + if (httpMessage.status === 503 && httpMessage.responseJSON.info !== undefined && + httpMessage.responseJSON.info === 'CONNECTION_LOST') { + setTimeout(function () { + this.sqlServerObject.save_state('execute', [this.explainPlan]); + this.sqlServerObject.handle_connection_lost(false, httpMessage); + }); + } + } + + this.sqlServerObject.update_msg_history(false, msg); + } + + removeGridViewMarker() { + if (this.sqlServerObject.gridView.marker) { + this.sqlServerObject.gridView.marker.clear(); + delete this.sqlServerObject.gridView.marker; + this.sqlServerObject.gridView.marker = null; + + // Remove already existing marker + this.sqlServerObject.gridView.query_tool_obj.removeLineClass(this.sqlServerObject.marked_line_no, 'wrap', 'CodeMirror-activeline-background'); + } + } + + enableSQLEditorButtons() { + this.sqlServerObject.disable_tool_buttons(false); + $('#btn-cancel-query').prop('disabled', true); + } + + disableSQLEditorButtons() { + this.sqlServerObject.disable_tool_buttons(true); + $('#btn-cancel-query').prop('disabled', false); + } + + static wasConnectionLostToServer(errorMessage) { + return errorMessage.readyState === 0; + } + + handleConnectionToServerLost() { + this.sqlServerObject.update_msg_history(false, + gettext('Not connected to the server or the connection to the server has been closed.') + ); + } + + updateSqlEditorStateWithInformationFromServer(messageData) { + this.sqlServerObject.can_edit = messageData.can_edit; + this.sqlServerObject.can_filter = messageData.can_filter; + this.sqlServerObject.info_notifier_timeout = messageData.info_notifier_timeout; + } + + static isSqlCorrect(httpMessageData) { + return httpMessageData.data.status; + } + + static extractErrorMessage(httpMessage) { + let msg = httpMessage.errormsg; + if (httpMessage.responseJSON !== undefined && + httpMessage.responseJSON.errormsg !== undefined) + msg = httpMessage.responseJSON.errormsg; + + return msg; + } + + static isQueryFinished(httpMessage) { + return httpMessage.data.data.status === 'Success'; + } + + static isQueryStillRunning(httpMessage) { + return httpMessage.data.data.status === 'Busy'; + } + + static isQueryCancelled(httpMessage) { + return httpMessage.data.data.status === 'Cancel'; + } + + static isConnectionToServerLostWhilePolling(httpMessage) { + return httpMessage.data.data.status === 'NotConnected'; + } +} + +module.exports = { + ExecuteQuery: ExecuteQuery, +}; diff --git a/web/pgadmin/static/js/sqleditor/is_new_transaction_required.js b/web/pgadmin/static/js/sqleditor/is_new_transaction_required.js new file mode 100644 index 00000000..9d83c926 --- /dev/null +++ b/web/pgadmin/static/js/sqleditor/is_new_transaction_required.js @@ -0,0 +1,14 @@ +////////////////////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////////////////// + +export function is_new_transaction_required(xhr) { + return xhr.status === 404 && xhr.responseJSON && + xhr.responseJSON.info && + xhr.responseJSON.info === 'DATAGRID_TRANSACTION_REQUIRED'; +} diff --git a/web/pgadmin/static/js/sqleditor/query_tool_actions.js b/web/pgadmin/static/js/sqleditor/query_tool_actions.js index 44a674b8..01d19215 100644 --- a/web/pgadmin/static/js/sqleditor/query_tool_actions.js +++ b/web/pgadmin/static/js/sqleditor/query_tool_actions.js @@ -2,19 +2,19 @@ import $ from 'jquery'; let queryToolActions = { _verbose: function () { - return $('.explain-verbose').hasClass('visibility-hidden') ? 'OFF' : 'ON'; + return !$('.explain-verbose').hasClass('visibility-hidden'); }, _costsEnabled: function () { - return $('.explain-costs').hasClass('visibility-hidden') ? 'OFF' : 'ON'; + return !$('.explain-costs').hasClass('visibility-hidden'); }, _buffers: function () { - return $('.explain-buffers').hasClass('visibility-hidden') ? 'OFF' : 'ON'; + return !$('.explain-buffers').hasClass('visibility-hidden'); }, _timing: function () { - return $('.explain-timing').hasClass('visibility-hidden') ? 'OFF' : 'ON'; + return !$('.explain-timing').hasClass('visibility-hidden'); }, _clearMessageTab: function () { @@ -35,18 +35,35 @@ let queryToolActions = { let verbose = this._verbose(); let buffers = this._buffers(); let timing = this._timing(); - let explainAnalyzeQuery = `EXPLAIN (FORMAT JSON, ANALYZE ON, VERBOSE ${verbose}, COSTS ${costEnabled}, BUFFERS ${buffers}, TIMING ${timing}) `; + const explainObject = { + format: 'json', + analyze: true, + verbose: verbose, + costs: costEnabled, + buffers: buffers, + timing: timing, + summary: false, + }; this._clearMessageTab(); - sqlEditorController.execute(explainAnalyzeQuery); + sqlEditorController.execute(explainObject); }, explain: function (sqlEditorController) { let costEnabled = this._costsEnabled(); let verbose = this._verbose(); - let explainQuery = `EXPLAIN (FORMAT JSON, ANALYZE OFF, VERBOSE ${verbose}, COSTS ${costEnabled}, BUFFERS OFF, TIMING OFF) `; + // let explainQuery = `EXPLAIN (FORMAT JSON, ANALYZE OFF, VERBOSE ${verbose}, COSTS ${costEnabled}, BUFFERS OFF, TIMING OFF) `; + const explainObject = { + format: 'json', + analyze: false, + verbose: verbose, + costs: costEnabled, + buffers: false, + timing: false, + summary: false, + }; this._clearMessageTab(); - sqlEditorController.execute(explainQuery); + sqlEditorController.execute(explainObject); }, download: function (sqlEditorController) { diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index b1ca3b87..f8870036 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -8,28 +8,32 @@ ########################################################################## """A blueprint module implementing the sqleditor frame.""" -import simplejson as json +import codecs import os import pickle import random -import codecs -from flask import Response, url_for, render_template, session, request,\ +import simplejson as json +from flask import Response, url_for, render_template, session, request, \ current_app from flask_babel import gettext from flask_security import login_required + +from config import PG_DEFAULT_DRIVER, ON_DEMAND_RECORD_COUNT +from pgadmin.misc.file_manager import Filemanager from pgadmin.tools.sqleditor.command import QueryToolCommand +from pgadmin.tools.sqleditor.utils.constant_definition import ASYNC_OK, ASYNC_EXECUTION_ABORTED, \ + CONNECTION_STATUS_MESSAGE_MAPPING, TX_STATUS_INERROR +from pgadmin.tools.sqleditor.utils.start_running_query import StartRunningQuery +from pgadmin.tools.sqleditor.utils.update_session_grid_transaction import update_session_grid_transaction from pgadmin.utils import PgAdminModule from pgadmin.utils import get_storage_directory from pgadmin.utils.ajax import make_json_response, bad_request, \ success_return, internal_server_error, unauthorized from pgadmin.utils.driver import get_driver -from pgadmin.utils.sqlautocomplete.autocomplete import SQLAutoComplete -from pgadmin.misc.file_manager import Filemanager from pgadmin.utils.menu import MenuItem from pgadmin.utils.exception import ConnectionLost - -from config import PG_DEFAULT_DRIVER, ON_DEMAND_RECORD_COUNT +from pgadmin.utils.sqlautocomplete.autocomplete import SQLAutoComplete MODULE_NAME = 'sqleditor' @@ -39,28 +43,6 @@ try: except ImportError: from urllib.parse import unquote -# Async Constants -ASYNC_OK = 1 -ASYNC_READ_TIMEOUT = 2 -ASYNC_WRITE_TIMEOUT = 3 -ASYNC_NOT_CONNECTED = 4 -ASYNC_EXECUTION_ABORTED = 5 - -# Transaction status constants -TX_STATUS_IDLE = 0 -TX_STATUS__ACTIVE = 1 -TX_STATUS_INTRANS = 2 -TX_STATUS_INERROR = 3 - -# Connection status codes mapping -CONNECTION_STATUS_MESSAGE_MAPPING = dict([ - (0, 'The session is idle and there is no current transaction.'), - (1, 'A command is currently in progress.'), - (2, 'The session is idle in a valid transaction block.'), - (3, 'The session is idle in a failed transaction block.'), - (4, 'The connection with the server is bad.') -]) - class SqlEditorModule(PgAdminModule): """ @@ -376,13 +358,6 @@ def index(): ) -def update_session_grid_transaction(trans_id, data): - if 'gridData' in session: - grid_data = session['gridData'] - grid_data[str(trans_id)] = data - session['gridData'] = grid_data - - def check_transaction_status(trans_id): """ This function is used to check the transaction id @@ -458,7 +433,7 @@ def start_view_data(trans_id): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) @@ -486,7 +461,7 @@ def start_view_data(trans_id): ) if status and conn is not None \ - and trans_obj is not None and session_obj is not None: + and trans_obj is not None and session_obj is not None: # set fetched row count to 0 as we are executing query again. trans_obj.update_fetched_row_cnt(0) session_obj['command_obj'] = pickle.dumps(trans_obj, -1) @@ -554,107 +529,19 @@ def start_query_tool(trans_id): trans_id: unique transaction id """ - if request.data: - sql = json.loads(request.data, encoding='utf-8') - else: - sql = request.args or request.form - - connect = True if 'connect' in request.args and \ - request.args['connect'] == '1' else False - - if 'gridData' not in session: - return make_json_response( - success=0, - errormsg=gettext('Transaction ID not found in the session.'), - info='DATAGRID_TRANSACTION_REQUIRED', status=404) - - grid_data = session['gridData'] - - # Return from the function if transaction id not found - if str(trans_id) not in grid_data: - return make_json_response( - success=0, - errormsg=gettext('Transaction ID not found in the session.'), - info='DATAGRID_TRANSACTION_REQUIRED', - status=404) - - # Fetch the object for the specified transaction id. - # Use pickle.loads function to get the command object - session_obj = grid_data[str(trans_id)] - trans_obj = pickle.loads(session_obj['command_obj']) - # set fetched row count to 0 as we are executing query again. - trans_obj.update_fetched_row_cnt(0) - - can_edit = False - can_filter = False + sql = extract_sql_from_network_parameters(request.data, request.args, request.form) - if trans_obj is not None and session_obj is not None: - conn_id = trans_obj.conn_id - try: - manager = get_driver( - PG_DEFAULT_DRIVER).connection_manager(trans_obj.sid) - conn = manager.connection(did=trans_obj.did, conn_id=conn_id, - auto_reconnect=False, - use_binary_placeholder=True, - array_to_string=True) - except ConnectionLost as e: - raise - except Exception as e: - current_app.logger.error(e) - return internal_server_error(errormsg=str(e)) + return StartRunningQuery(blueprint, current_app).execute(sql, trans_id, session) - # Connect to the Server if not connected. - if connect and not conn.connected(): - status, msg = conn.connect() - if not status: - current_app.logger.error(msg) - return internal_server_error(errormsg=str(msg)) - - # on successful connection set the connection id to the - # transaction object - trans_obj.set_connection_id(conn_id) - - # As we changed the transaction object we need to - # restore it and update the session variable. - session_obj['command_obj'] = pickle.dumps(trans_obj, -1) - update_session_grid_transaction(trans_id, session_obj) - - # If auto commit is False and transaction status is Idle - # then call is_begin_not_required() function to check BEGIN - # is required or not. - - if not trans_obj.auto_commit \ - and conn.transaction_status() == TX_STATUS_IDLE \ - and is_begin_required(sql): - conn.execute_void("BEGIN;") - - # Execute sql asynchronously with params is None - # and formatted_error is True. - try: - status, result = conn.execute_async(sql) - except ConnectionLost as e: - raise - # If the transaction aborted for some reason and - # Auto RollBack is True then issue a rollback to cleanup. - trans_status = conn.transaction_status() - if trans_status == TX_STATUS_INERROR and trans_obj.auto_rollback: - conn.execute_void("ROLLBACK;") - - can_edit = trans_obj.can_edit() - can_filter = trans_obj.can_filter() +def extract_sql_from_network_parameters(request_data, request_arguments, request_form_data): + if request_data: + sql_parameters = json.loads(request_data, encoding='utf-8') + if type(sql_parameters) is str: + return dict(sql=sql_parameters, explain_plan=None) + return sql_parameters else: - status = False - result = gettext( - 'Either transaction object or session object not found.') - - return make_json_response( - data={ - 'status': status, 'result': result, - 'can_edit': can_edit, 'can_filter': can_filter, - 'info_notifier_timeout': blueprint.info_notifier_timeout.get() - } - ) + return request_arguments or request_form_data @blueprint.route( @@ -675,13 +562,13 @@ def preferences(trans_id): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) if status and conn is not None \ - and trans_obj is not None and session_obj is not None: + and trans_obj is not None and session_obj is not None: # Call the set_auto_commit and set_auto_rollback method of # transaction object trans_obj.set_auto_commit(blueprint.auto_commit.get()) @@ -751,7 +638,7 @@ def poll(trans_id): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) @@ -779,7 +666,7 @@ def poll(trans_id): if isinstance(trans_obj, QueryToolCommand): trans_status = conn.transaction_status() if (trans_status == TX_STATUS_INERROR and - trans_obj.auto_rollback): + trans_obj.auto_rollback): conn.execute_void("ROLLBACK;") st, result = conn.async_fetchmany_2darray(ON_DEMAND_RECORD_COUNT) @@ -854,15 +741,15 @@ def poll(trans_id): typname == 'character varying' ): typname = typname + '(' + \ - str(col_info['internal_size']) + \ - ')' + str(col_info['internal_size']) + \ + ')' elif ( typname == 'character[]' or typname == 'character varying[]' ): typname = typname[:-2] + '(' + \ - str(col_info['internal_size']) + \ - ')[]' + str(col_info['internal_size']) + \ + ')[]' col_info['type_name'] = typname @@ -913,7 +800,7 @@ def poll(trans_id): if status == 'Success' and result is None: result = conn.status_message() if (result != 'SELECT 1' or result != 'SELECT 0') \ - and result is not None and additional_messages: + and result is not None and additional_messages: result = additional_messages + result return make_json_response( @@ -954,7 +841,7 @@ def fetch(trans_id, fetch_all=None): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) @@ -1018,7 +905,7 @@ def fetch_pg_types(columns_info, trans_obj): if oids: status, res = default_conn.execute_dict( - u"SELECT oid, format_type(oid,null) as typname FROM pg_type " + u"SELECT oid, format_type(oid,NULL) AS typname FROM pg_type " u"WHERE oid IN %s ORDER BY oid;", [tuple(oids)] ) @@ -1077,17 +964,17 @@ def save(trans_id): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) if status and conn is not None \ - and trans_obj is not None and session_obj is not None: + and trans_obj is not None and session_obj is not None: # If there is no primary key found then return from the function. if (len(session_obj['primary_keys']) <= 0 or len(changed_data) <= 0) \ - and 'has_oids' not in session_obj: + and 'has_oids' not in session_obj: return make_json_response( data={ 'status': False, @@ -1146,12 +1033,12 @@ def get_filter(trans_id): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) if status and conn is not None \ - and trans_obj is not None and session_obj is not None: + and trans_obj is not None and session_obj is not None: res = trans_obj.get_filter() else: @@ -1183,13 +1070,13 @@ def apply_filter(trans_id): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) if status and conn is not None \ - and trans_obj is not None and session_obj is not None: + and trans_obj is not None and session_obj is not None: status, res = trans_obj.set_filter(filter_sql) @@ -1226,13 +1113,13 @@ def append_filter_inclusive(trans_id): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) if status and conn is not None \ - and trans_obj is not None and session_obj is not None: + and trans_obj is not None and session_obj is not None: res = None filter_sql = '' @@ -1282,12 +1169,12 @@ def append_filter_exclusive(trans_id): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) if status and conn is not None \ - and trans_obj is not None and session_obj is not None: + and trans_obj is not None and session_obj is not None: res = None filter_sql = '' @@ -1335,13 +1222,13 @@ def remove_filter(trans_id): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) if status and conn is not None \ - and trans_obj is not None and session_obj is not None: + and trans_obj is not None and session_obj is not None: res = None @@ -1380,13 +1267,13 @@ def set_limit(trans_id): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) if status and conn is not None \ - and trans_obj is not None and session_obj is not None: + and trans_obj is not None and session_obj is not None: res = None @@ -1501,13 +1388,13 @@ def get_object_name(trans_id): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) if status and conn is not None \ - and trans_obj is not None and session_obj is not None: + and trans_obj is not None and session_obj is not None: res = trans_obj.object_name else: status = False @@ -1538,13 +1425,13 @@ def set_auto_commit(trans_id): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) if status and conn is not None \ - and trans_obj is not None and session_obj is not None: + and trans_obj is not None and session_obj is not None: res = None @@ -1587,13 +1474,13 @@ def set_auto_rollback(trans_id): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) if status and conn is not None \ - and trans_obj is not None and session_obj is not None: + and trans_obj is not None and session_obj is not None: res = None @@ -1643,13 +1530,13 @@ def auto_complete(trans_id): check_transaction_status(trans_id) if error_msg == gettext( - 'Transaction ID not found in the session.'): + 'Transaction ID not found in the session.'): return make_json_response(success=0, errormsg=error_msg, info='DATAGRID_TRANSACTION_REQUIRED', status=404) if status and conn is not None \ - and trans_obj is not None and session_obj is not None: + and trans_obj is not None and session_obj is not None: # Create object of SQLAutoComplete class and pass connection object auto_complete_obj = SQLAutoComplete( @@ -1680,165 +1567,6 @@ def script(): ) -def is_begin_required(query): - word_len = 0 - query = query.strip() - query_len = len(query) - - # Check word length (since "beginx" is not "begin"). - while (word_len < query_len) and query[word_len].isalpha(): - word_len += 1 - - # Transaction control commands. These should include every keyword that - # gives rise to a TransactionStmt in the backend grammar, except for the - # savepoint-related commands. - # - # (We assume that START must be START TRANSACTION, since there is - # presently no other "START foo" command.) - - keyword = query[0:word_len] - - if word_len == 5 and keyword.lower() == "abort": - return False - if word_len == 5 and keyword.lower() == "begin": - return False - if word_len == 5 and keyword.lower() == "start": - return False - if word_len == 6: - # SELECT is protected from dirty reads hence don't require transaction - if keyword.lower() in ["select", "commit"]: - return False - if word_len == 3 and keyword.lower() == "end": - return False - if word_len == 8 and keyword.lower() == "rollback": - return False - if word_len == 7 and keyword.lower() == "prepare": - # PREPARE TRANSACTION is a TC command, PREPARE foo is not - query = query[word_len:query_len] - query = query.strip() - query_len = len(query) - word_len = 0 - - while (word_len < query_len) and query[word_len].isalpha(): - word_len += 1 - - keyword = query[0:word_len] - if word_len == 11 and keyword.lower() == "transaction": - return False - return True - - # Commands not allowed within transactions. The statements checked for - # here should be exactly those that call PreventTransactionChain() in the - # backend. - if word_len == 6 and keyword.lower() == "vacuum": - return False - - if word_len == 7 and keyword.lower() == "cluster": - # CLUSTER with any arguments is allowed in transactions - query = query[word_len:query_len] - query = query.strip() - - if query[0].isalpha(): - return True # has additional words - return False # it's CLUSTER without arguments - - if word_len == 6 and keyword.lower() == "create": - query = query[word_len:query_len] - query = query.strip() - query_len = len(query) - word_len = 0 - - while (word_len < query_len) and query[word_len].isalpha(): - word_len += 1 - - keyword = query[0:word_len] - if word_len == 8 and keyword.lower() == "database": - return False - if word_len == 10 and keyword.lower() == "tablespace": - return False - - # CREATE [UNIQUE] INDEX CONCURRENTLY isn't allowed in xacts - if word_len == 7 and keyword.lower() == "cluster": - query = query[word_len:query_len] - query = query.strip() - query_len = len(query) - word_len = 0 - - while (word_len < query_len) and query[word_len].isalpha(): - word_len += 1 - - keyword = query[0:word_len] - - if word_len == 5 and keyword.lower() == "index": - query = query[word_len:query_len] - query = query.strip() - query_len = len(query) - word_len = 0 - - while (word_len < query_len) and query[word_len].isalpha(): - word_len += 1 - - keyword = query[0:word_len] - if word_len == 12 and keyword.lower() == "concurrently": - return False - return True - - if word_len == 5 and keyword.lower() == "alter": - query = query[word_len:query_len] - query = query.strip() - query_len = len(query) - word_len = 0 - - while (word_len < query_len) and query[word_len].isalpha(): - word_len += 1 - - keyword = query[0:word_len] - - # ALTER SYSTEM isn't allowed in xacts - if word_len == 6 and keyword.lower() == "system": - return False - return True - - # Note: these tests will match DROP SYSTEM and REINDEX TABLESPACE, which - # aren't really valid commands so we don't care much. The other four - # possible matches are correct. - if word_len == 4 and keyword.lower() == "drop" \ - or word_len == 7 and keyword.lower() == "reindex": - query = query[word_len:query_len] - query = query.strip() - query_len = len(query) - word_len = 0 - - while (word_len < query_len) and query[word_len].isalpha(): - word_len += 1 - - keyword = query[0:word_len] - if word_len == 8 and keyword.lower() == "database": - return False - if word_len == 6 and keyword.lower() == "system": - return False - if word_len == 10 and keyword.lower() == "tablespace": - return False - return True - - # DISCARD ALL isn't allowed in xacts, but other variants are allowed. - if word_len == 7 and keyword.lower() == "discard": - query = query[word_len:query_len] - query = query.strip() - query_len = len(query) - word_len = 0 - - while (word_len < query_len) and query[word_len].isalpha(): - word_len += 1 - - keyword = query[0:word_len] - if word_len == 3 and keyword.lower() == "all": - return False - return True - - return True - - @blueprint.route('/load_file/', methods=["PUT", "POST"], endpoint='load_file') @login_required def load_file(): @@ -1865,9 +1593,9 @@ def load_file(): ) status, err_msg, is_binary, \ - is_startswith_bom, enc = Filemanager.check_file_for_bom_and_binary( - file_path - ) + is_startswith_bom, enc = Filemanager.check_file_for_bom_and_binary( + file_path + ) if not status: return internal_server_error( @@ -1960,10 +1688,10 @@ def save_file(): def start_query_download_tool(trans_id): sync_conn = None status, error_msg, conn, trans_obj, \ - session_obj = check_transaction_status(trans_id) + session_obj = check_transaction_status(trans_id) if status and conn is not None \ - and trans_obj is not None and session_obj is not None: + and trans_obj is not None and session_obj is not None: data = request.args if request.args else None try: @@ -2063,7 +1791,7 @@ def query_tool_status(trans_id): TRANSACTION_STATUS_UNKNOWN = 4 """ status, error_msg, conn, trans_obj, \ - session_obj = check_transaction_status(trans_id) + session_obj = check_transaction_status(trans_id) if not status and error_msg and type(error_msg) == str: return internal_server_error( diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index 52109ecd..349c9a00 100644 --- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js @@ -12,6 +12,8 @@ define('tools.querytool', [ 'sources/selection/xcell_selection_model', 'sources/selection/set_staged_rows', 'sources/sqleditor_utils', + 'sources/sqleditor/execute_query', + 'sources/sqleditor/is_new_transaction_required', 'sources/history/index.js', 'sources/../jsx/history/query_history', 'react', 'react-dom', @@ -28,7 +30,8 @@ define('tools.querytool', [ ], function( babelPollyfill, gettext, url_for, $, _, S, alertify, pgAdmin, Backbone, codemirror, pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent, - XCellSelectionModel, setStagedRows, SqlEditorUtils, HistoryBundle, queryHistory, React, ReactDOM, + XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, transaction, + HistoryBundle, queryHistory, React, ReactDOM, keyboardShortcuts, queryToolActions, Datagrid) { /* Return back, this has been called more than once */ if (pgAdmin.SqlEditor) @@ -43,12 +46,6 @@ define('tools.querytool', [ var is_query_running = false; - var is_new_transaction_required = function(xhr) { - return xhr.status == 404 && xhr.responseJSON && - xhr.responseJSON.info && - xhr.responseJSON.info == 'DATAGRID_TRANSACTION_REQUIRED'; - }; - // Defining Backbone view for the sql grid. var SQLEditorView = Backbone.View.extend({ initialize: function(opts) { @@ -485,7 +482,7 @@ define('tools.querytool', [ if (pgAdmin.Browser.UserManagement.is_pga_login_required(e)) { return pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { return self.init_transaction(); } }, @@ -2188,7 +2185,7 @@ define('tools.querytool', [ pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('_run_query', []); self.init_transaction(); } @@ -2256,74 +2253,8 @@ define('tools.querytool', [ * 'Success' then call the render method to render the data. */ _poll: function() { - var self = this; - - setTimeout( - function() { - $.ajax({ - url: url_for('sqleditor.poll', { - 'trans_id': self.transId, - }), - method: 'GET', - success: function(res) { - if (res.data.status === 'Success') { - self.trigger( - 'pgadmin-sqleditor:loading-icon:message', - gettext('Loading data from the database server and rendering...') - ); - - self.call_render_after_poll(res.data); - } else if (res.data.status === 'Busy') { - // If status is Busy then poll the result by recursive call to the poll function - self._poll(); - is_query_running = true; - if (res.data.result) { - self.update_msg_history(res.data.status, res.data.result, false); - } - } else if (res.data.status === 'NotConnected') { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - // Enable/Disable query tool button only if is_query_tool is true. - if (self.is_query_tool) { - self.disable_tool_buttons(false); - $('#btn-cancel-query').prop('disabled', true); - } - self.update_msg_history(false, res.data.result, true); - } else if (res.data.status === 'Cancel') { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - self.update_msg_history(false, 'Execution Cancelled!', true); - } - }, - error: function(e) { - // Enable/Disable query tool button only if is_query_tool is true. - self.resetQueryHistoryObject(self); - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - if (self.is_query_tool) { - self.disable_tool_buttons(false); - $('#btn-cancel-query').prop('disabled', true); - } - - if (e.readyState == 0) { - self.update_msg_history(false, - gettext('Not connected to the server or the connection to the server has been closed.') - ); - return; - } - - if (pgAdmin.Browser.UserManagement.is_pga_login_required(e)) { - return pgAdmin.Browser.UserManagement.pga_login(); - } - - var msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; - - self.update_msg_history(false, msg); - // Highlight the error in the sql panel - self._highlight_error(msg); - }, - }); - }, self.POLL_FALLBACK_TIME()); + const executeQuery = new ExecuteQuery.ExecuteQuery(this, pgAdmin.Browser.UserManagement); + executeQuery.delayedPoll(this); }, /* This function is used to create the backgrid columns, @@ -2941,7 +2872,7 @@ define('tools.querytool', [ pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('_save', [view, controller, save_as]); self.init_transaction(); } @@ -3267,7 +3198,7 @@ define('tools.querytool', [ return pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('_show_filter', []); return self.init_transaction(); } @@ -3356,7 +3287,7 @@ define('tools.querytool', [ return pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('_include_filter', []); return self.init_transaction(); } @@ -3447,7 +3378,7 @@ define('tools.querytool', [ return pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('_exclude_filter', []); return self.init_transaction(); } @@ -3517,7 +3448,7 @@ define('tools.querytool', [ return pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('_remove_filter', []); return self.init_transaction(); } @@ -3592,7 +3523,7 @@ define('tools.querytool', [ return pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('_apply_filter', []); return self.init_transaction(); } @@ -3747,7 +3678,7 @@ define('tools.querytool', [ return pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('_set_limit', []); return self.init_transaction(); } @@ -3795,10 +3726,7 @@ define('tools.querytool', [ // and execute the query. execute: function(explain_prefix) { var self = this, - sql = '', - url = url_for('sqleditor.query_tool_start', { - 'trans_id': self.transId, - }); + sql = ''; self.has_more_rows = false; self.fetching_rows = false; @@ -3812,109 +3740,8 @@ define('tools.querytool', [ else sql = self.gridView.query_tool_obj.getValue(); - // If it is an empty query, do nothing. - if (sql.length <= 0) return; - - self.trigger( - 'pgadmin-sqleditor:loading-icon:show', - gettext('Initializing query execution...') - ); - - $('#btn-flash').prop('disabled', true); - - if (explain_prefix != undefined && - !S.startsWith(sql.trim().toUpperCase(), 'EXPLAIN')) { - sql = explain_prefix + ' ' + sql; - } - - self.query_start_time = new Date(); - self.query = sql; - self.rows_affected = 0; - self._init_polling_flags(); - self.disable_tool_buttons(true); - $('#btn-cancel-query').prop('disabled', false); - - if (arguments.length > 0 && - arguments[arguments.length - 1] == 'connect') { - url += '?connect=1'; - } - - $.ajax({ - url: url, - method: 'POST', - contentType: 'application/json', - data: JSON.stringify(sql), - success: function(res) { - // Remove marker - if (self.gridView.marker) { - self.gridView.marker.clear(); - delete self.gridView.marker; - self.gridView.marker = null; - - // Remove already existing marker - self.gridView.query_tool_obj.removeLineClass(self.marked_line_no, 'wrap', 'CodeMirror-activeline-background'); - } - - if (res.data.status) { - self.trigger( - 'pgadmin-sqleditor:loading-icon:message', - gettext('Waiting for the query execution to complete...') - ); - - self.can_edit = res.data.can_edit; - self.can_filter = res.data.can_filter; - self.info_notifier_timeout = res.data.info_notifier_timeout; - - // If status is True then poll the result. - self._poll(); - } else { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - self.disable_tool_buttons(false); - $('#btn-cancel-query').prop('disabled', true); - self.update_msg_history(false, res.data.result); - - // Highlight the error in the sql panel - self._highlight_error(res.data.result); - } - }, - error: function(e) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - self.disable_tool_buttons(false); - $('#btn-cancel-query').prop('disabled', true); - - if (e.readyState == 0) { - self.update_msg_history(false, - gettext('Not connected to the server or the connection to the server has been closed.') - ); - return; - } - - if (pgAdmin.Browser.UserManagement.is_pga_login_required(e)) { - self.save_state('execute', [explain_prefix]); - pgAdmin.Browser.UserManagement.pga_login(); - } - - if(is_new_transaction_required(e)) { - self.save_state('execute', [explain_prefix]); - self.init_transaction(); - } - var msg = e.responseText; - if (e.responseJSON != undefined) { - if(e.responseJSON.errormsg != undefined) { - msg = e.responseJSON.errormsg; - } - - if(e.status == 503 && e.responseJSON.info != undefined && - e.responseJSON.info == 'CONNECTION_LOST') { - setTimeout(function() { - self.save_state('execute', [explain_prefix]); - self.handle_connection_lost(false, e); - }); - } - } - self.update_msg_history(false, msg); - }, - }); + const executeQuery = new ExecuteQuery.ExecuteQuery(this, pgAdmin.Browser.UserManagement); + executeQuery.execute(sql, explain_prefix); }, /* This function is used to highlight the error line and @@ -4078,7 +3905,7 @@ define('tools.querytool', [ return pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('_auto_rollback', []); self.init_transaction(); } @@ -4139,7 +3966,7 @@ define('tools.querytool', [ return pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('_auto_commit', []); return self.init_transaction(); } @@ -4201,7 +4028,7 @@ define('tools.querytool', [ return pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('_explain_verbose', []); return self.init_transaction(); } @@ -4250,7 +4077,7 @@ define('tools.querytool', [ return pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('_explain_costs', []); return self.init_transaction(); } @@ -4298,7 +4125,7 @@ define('tools.querytool', [ return pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('_explain_buffers', []); return self.init_transaction(); } @@ -4345,7 +4172,7 @@ define('tools.querytool', [ return pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('_explain_timing', []); return self.init_transaction(); } @@ -4379,6 +4206,10 @@ define('tools.querytool', [ return is_query_running; }, + setIsQueryRunning: function(value) { + is_query_running = value; + }, + /* * This function get explain options and auto rollback/auto commit * values from preferences @@ -4455,7 +4286,7 @@ define('tools.querytool', [ return pgAdmin.Browser.UserManagement.pga_login(); } - if(is_new_transaction_required(e)) { + if(transaction.is_new_transaction_required(e)) { self.save_state('get_preferences', []); return self.init_transaction(); } diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/10_plus/explain_plan.sql b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/10_plus/explain_plan.sql new file mode 100644 index 00000000..fc4a8d2d --- /dev/null +++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/10_plus/explain_plan.sql @@ -0,0 +1,23 @@ +EXPLAIN ( +{% if format %} + FORMAT {{ format.upper() }}, +{% endif %} +{% if analyze is defined %} + ANALYZE {{ analyze }}, +{% endif %} +{% if verbose is defined %} + VERBOSE {{ verbose }}, +{% endif %} +{% if costs is defined %} + COSTS {{ costs }}, +{% endif %} +{% if timing is defined %} + TIMING {{ timing }}, +{% endif %} +{% if summary is defined %} + SUMMARY {{ summary }}, +{% endif %} +{% if buffers is defined %} + BUFFERS {{ buffers }} +{% endif %} +) {{ sql }} diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/9.2_plus/explain_plan.sql b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/9.2_plus/explain_plan.sql new file mode 100644 index 00000000..315174a6 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/9.2_plus/explain_plan.sql @@ -0,0 +1,20 @@ +EXPLAIN ( +{% if format %} + FORMAT {{ format.upper() }}, +{% endif %} +{% if analyze is defined %} + ANALYZE {{ analyze }}, +{% endif %} +{% if verbose is defined %} + VERBOSE {{ verbose }}, +{% endif %} +{% if costs is defined %} + COSTS {{ costs }}, +{% endif %} +{% if timing is defined %} + TIMING {{ timing }}, +{% endif %} +{% if buffers is defined %} + BUFFERS {{ buffers }} +{% endif %} +) {{ sql }} diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/explain_plan.sql b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/explain_plan.sql new file mode 100644 index 00000000..72b20c95 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/explain_plan.sql @@ -0,0 +1,17 @@ +EXPLAIN ( +{% if format %} + FORMAT {{ format.upper() }}, +{% endif %} +{% if analyze is defined %} + ANALYZE {{ analyze }}, +{% endif %} +{% if verbose is defined %} + VERBOSE {{ verbose }}, +{% endif %} +{% if costs is defined %} + COSTS {{ costs }}, +{% endif %} +{% if buffers is defined %} + BUFFERS {{ buffers }} +{% endif %} +) {{ sql }} diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/gpdb_5.0_plus/explain_plan.sql b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/gpdb_5.0_plus/explain_plan.sql new file mode 100644 index 00000000..aacabe45 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/gpdb_5.0_plus/explain_plan.sql @@ -0,0 +1,5 @@ +EXPLAIN +{% if analyze %} + ANALYZE +{% endif %} + {{ sql }} diff --git a/web/pgadmin/tools/sqleditor/tests/__init__.py b/web/pgadmin/tools/sqleditor/tests/__init__.py new file mode 100644 index 00000000..590026ad --- /dev/null +++ b/web/pgadmin/tools/sqleditor/tests/__init__.py @@ -0,0 +1,8 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## diff --git a/web/pgadmin/tools/sqleditor/tests/test_explain_plan_templates.py b/web/pgadmin/tools/sqleditor/tests/test_explain_plan_templates.py new file mode 100644 index 00000000..ab018788 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/tests/test_explain_plan_templates.py @@ -0,0 +1,152 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import os + +from flask import Flask, render_template +from jinja2 import FileSystemLoader + +from pgadmin import VersionedTemplateLoader +from pgadmin.utils.route import BaseTestGenerator + + +class TestExplainPlanTemplates(BaseTestGenerator): + scenarios = [ + ( + 'When rendering Postgres 9.0 template, ' + 'when passing all parameters,' + 'it returns the explain plan with all parameters', + dict( + template_path=os.path.join('sqleditor', 'sql', 'default', 'explain_plan.sql'), + input_parameters=dict( + sql='select * from places', + format='xml', + analyze=True, + verbose=True, + costs=False, + buffers=True + ), + sql_statement='select * from places', + expected_return_value='EXPLAIN ' + '( FORMAT XML, ANALYZE True, VERBOSE True, ' + 'COSTS False, BUFFERS True) select * from places' + ) + ), + ( + 'When rendering Postgres 9.0 template, ' + 'when not all parameters are present,' + 'it returns the explain plan with the present parameters', + dict( + template_path=os.path.join('sqleditor', 'sql', 'default', 'explain_plan.sql'), + input_parameters=dict( + sql='select * from places', + format='json', + buffers=True + ), + sql_statement='select * from places', + expected_return_value='EXPLAIN ' + '( FORMAT JSON, BUFFERS True) select * from places' + ) + ), + ( + 'When rendering Postgres 9.2 template, ' + 'when timing is present,' + 'it returns the explain plan with timing', + dict( + template_path=os.path.join('sqleditor', 'sql', '9.2_plus', 'explain_plan.sql'), + input_parameters=dict( + sql='select * from places', + format='json', + buffers=True, + timing=False + ), + sql_statement='select * from places', + expected_return_value='EXPLAIN ' + '( FORMAT JSON, TIMING False, BUFFERS True) select * from places' + ) + ), + ( + 'When rendering Postgres 10 template, ' + 'when summary is present,' + 'it returns the explain plan with summary', + dict( + template_path=os.path.join('sqleditor', 'sql', '10_plus', 'explain_plan.sql'), + input_parameters=dict( + sql='select * from places', + format='yaml', + buffers=True, + timing=False, + summary=True + ), + sql_statement='select * from places', + expected_return_value='EXPLAIN ' + '( FORMAT YAML, TIMING False, SUMMARY True, BUFFERS True) select * from places' + ) + ), + ( + 'When rendering GreenPlum 5.3 template, ' + 'when all parameters are present,' + 'it returns the explain without parameters', + dict( + template_path=os.path.join('sqleditor', 'sql', 'gpdb_5.0_plus', 'explain_plan.sql'), + input_parameters=dict( + sql='select * from places', + format='json', + buffers=True + ), + sql_statement='select * from places', + expected_return_value='EXPLAIN select * from places' + ) + ), + ( + 'When rendering GreenPlum 5.3 template, ' + 'when analyze is true,' + 'it returns the explain analyze', + dict( + template_path=os.path.join('sqleditor', 'sql', 'gpdb_5.0_plus', 'explain_plan.sql'), + input_parameters=dict( + sql='select * from places', + analyze=True + ), + sql_statement='select * from places', + expected_return_value='EXPLAIN ANALYZE select * from places' + ) + ), + ( + 'When rendering GreenPlum 5.3 template, ' + 'when analyze is false,' + 'it returns the only explain', + dict( + template_path=os.path.join('sqleditor', 'sql', 'gpdb_5.0_plus', 'explain_plan.sql'), + input_parameters=dict( + sql='select * from places', + analyze=False + ), + sql_statement='select * from places', + expected_return_value='EXPLAIN select * from places' + ) + ), + ] + + def setUp(self): + self.loader = VersionedTemplateLoader(FakeApp()) + + def runTest(self): + with FakeApp().app_context(): + result = render_template(self.template_path, **self.input_parameters) + self.assertEqual( + str(result).replace("\n", ""), self.expected_return_value) + + +class FakeApp(Flask): + def __init__(self): + super(FakeApp, self).__init__("") + self.jinja_loader = FileSystemLoader( + os.path.dirname(os.path.realpath(__file__)) + "/../templates" + ) diff --git a/web/pgadmin/tools/sqleditor/tests/test_extract_sql_from_network_parameters.py b/web/pgadmin/tools/sqleditor/tests/test_extract_sql_from_network_parameters.py new file mode 100644 index 00000000..c4e002cc --- /dev/null +++ b/web/pgadmin/tools/sqleditor/tests/test_extract_sql_from_network_parameters.py @@ -0,0 +1,59 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from werkzeug.datastructures import ImmutableMultiDict + +from pgadmin.tools.sqleditor import extract_sql_from_network_parameters +from pgadmin.utils.route import BaseTestGenerator + + +class ExtractSQLFromNetworkParametersTest(BaseTestGenerator): + """ + This class validates the change password functionality + by defining change password scenarios; where dict of + parameters describes the scenario appended by test name. + """ + + scenarios = [ + ('Single string in the payload', dict( + request_strigified_data='"some sql"', + request_arguments=ImmutableMultiDict(), + request_form_data=ImmutableMultiDict(), + + expected_result=dict(sql='some sql', explain_plan=None) + )), + ('Payload that requests explain plan using json', dict( + request_strigified_data='{"sql": "some sql", "explain_plan": {"format": "json", "analyze": false, "verbose": false, "costs": false, "buffers": false, "timing": false}}', + request_arguments=ImmutableMultiDict(), + request_form_data=ImmutableMultiDict(), + + expected_result=dict( + sql='some sql', + explain_plan=dict( + format='json', + analyze=False, + verbose=False, + buffers=False, + costs=False, + timing=False + ) + ) + )) + ] + + def runTest(self): + """Check correct function is called to handle to run query.""" + + result = extract_sql_from_network_parameters( + self.request_strigified_data, + self.request_arguments, + self.request_form_data + ) + + self.assertEquals(result, self.expected_result) diff --git a/web/pgadmin/tools/sqleditor/tests/test_start_query_tool.py b/web/pgadmin/tools/sqleditor/tests/test_start_query_tool.py new file mode 100644 index 00000000..5e2752ae --- /dev/null +++ b/web/pgadmin/tools/sqleditor/tests/test_start_query_tool.py @@ -0,0 +1,38 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +import sys + +from pgadmin.utils.route import BaseTestGenerator +from pgadmin.tools.sqleditor import StartRunningQuery + +if sys.version_info < (3, 3): + from mock import patch, ANY +else: + from unittest.mock import patch, ANY + + +class StartQueryTool(BaseTestGenerator): + """ + Ensures that the call to the backend to start running a query + calls the needed functions + """ + + @patch('pgadmin.tools.sqleditor.extract_sql_from_network_parameters') + def runTest(self, extract_sql_from_network_parameters_mock): + """Check correct function is called to handle to run query.""" + + extract_sql_from_network_parameters_mock.return_value = 'transformed sql' + + with patch.object(StartRunningQuery, 'execute', return_value='some result') as StartRunningQuery_execute_mock: + response = self.tester.post('/sqleditor/query_tool/start/1234', data='"some sql statement"') + + self.assertEquals(response.status, '200 OK') + self.assertEquals(response.data, 'some result') + StartRunningQuery_execute_mock.assert_called_with('transformed sql', 1234, ANY) + extract_sql_from_network_parameters_mock.assert_called_with('"some sql statement"', ANY, ANY) diff --git a/web/pgadmin/tools/sqleditor/utils/__init__.py b/web/pgadmin/tools/sqleditor/utils/__init__.py new file mode 100644 index 00000000..3626b658 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/__init__.py @@ -0,0 +1,14 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from .constant_definition import * +from .is_begin_required import is_begin_required +from .update_session_grid_transaction import update_session_grid_transaction +from .start_running_query import * +from .apply_explain_plan_wrapper import * diff --git a/web/pgadmin/tools/sqleditor/utils/apply_explain_plan_wrapper.py b/web/pgadmin/tools/sqleditor/utils/apply_explain_plan_wrapper.py new file mode 100644 index 00000000..84e478de --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/apply_explain_plan_wrapper.py @@ -0,0 +1,24 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Apply Explain plan wrapper to sql object.""" +from flask import render_template + +from pgadmin.utils.compile_template_name import compile_template_name + + +def apply_explain_plan_wrapper_if_needed(manager, sql): + if 'explain_plan' in sql and sql['explain_plan']: + explain_plan = sql['explain_plan'] + ver = manager.version + server_type = manager.server_type + template_path = compile_template_name('sqleditor/sql', 'explain_plan.sql', server_type, ver) + return render_template(template_path, sql=sql['sql'], **explain_plan) + else: + return sql['sql'] diff --git a/web/pgadmin/tools/sqleditor/utils/constant_definition.py b/web/pgadmin/tools/sqleditor/utils/constant_definition.py new file mode 100644 index 00000000..44c7ecb7 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/constant_definition.py @@ -0,0 +1,32 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Definition of constants for SQLEditor.""" + +# Async Constants +ASYNC_OK = 1 +ASYNC_READ_TIMEOUT = 2 +ASYNC_WRITE_TIMEOUT = 3 +ASYNC_NOT_CONNECTED = 4 +ASYNC_EXECUTION_ABORTED = 5 + +# Transaction status constants +TX_STATUS_IDLE = 0 +TX_STATUS__ACTIVE = 1 +TX_STATUS_INTRANS = 2 +TX_STATUS_INERROR = 3 + +# Connection status codes mapping +CONNECTION_STATUS_MESSAGE_MAPPING = dict({ + 0: 'The session is idle and there is no current transaction.', + 1: 'A command is currently in progress.', + 2: 'The session is idle in a valid transaction block.', + 3: 'The session is idle in a failed transaction block.', + 4: 'The connection with the server is bad.' +}) diff --git a/web/pgadmin/tools/sqleditor/utils/is_begin_required.py b/web/pgadmin/tools/sqleditor/utils/is_begin_required.py new file mode 100644 index 00000000..8db7b954 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/is_begin_required.py @@ -0,0 +1,169 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Check if requires BEGIN in the current query.""" + + +def is_begin_required(query): + word_len = 0 + query = query.strip() + query_len = len(query) + + # Check word length (since "beginx" is not "begin"). + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + # Transaction control commands. These should include every keyword that + # gives rise to a TransactionStmt in the backend grammar, except for the + # savepoint-related commands. + # + # (We assume that START must be START TRANSACTION, since there is + # presently no other "START foo" command.) + + keyword = query[0:word_len] + + if word_len == 5 and keyword.lower() == "abort": + return False + if word_len == 5 and keyword.lower() == "begin": + return False + if word_len == 5 and keyword.lower() == "start": + return False + if word_len == 6: + # SELECT is protected from dirty reads hence don't require transaction + if keyword.lower() in ["select", "commit"]: + return False + if word_len == 3 and keyword.lower() == "end": + return False + if word_len == 8 and keyword.lower() == "rollback": + return False + if word_len == 7 and keyword.lower() == "prepare": + # PREPARE TRANSACTION is a TC command, PREPARE foo is not + query = query[word_len:query_len] + query = query.strip() + query_len = len(query) + word_len = 0 + + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + keyword = query[0:word_len] + if word_len == 11 and keyword.lower() == "transaction": + return False + return True + + # Commands not allowed within transactions. The statements checked for + # here should be exactly those that call PreventTransactionChain() in the + # backend. + if word_len == 6 and keyword.lower() == "vacuum": + return False + + if word_len == 7 and keyword.lower() == "cluster": + # CLUSTER with any arguments is allowed in transactions + query = query[word_len:query_len] + query = query.strip() + + if query[0].isalpha(): + return True # has additional words + return False # it's CLUSTER without arguments + + if word_len == 6 and keyword.lower() == "create": + query = query[word_len:query_len] + query = query.strip() + query_len = len(query) + word_len = 0 + + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + keyword = query[0:word_len] + if word_len == 8 and keyword.lower() == "database": + return False + if word_len == 10 and keyword.lower() == "tablespace": + return False + + # CREATE [UNIQUE] INDEX CONCURRENTLY isn't allowed in xacts + if word_len == 7 and keyword.lower() == "cluster": + query = query[word_len:query_len] + query = query.strip() + query_len = len(query) + word_len = 0 + + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + keyword = query[0:word_len] + + if word_len == 5 and keyword.lower() == "index": + query = query[word_len:query_len] + query = query.strip() + query_len = len(query) + word_len = 0 + + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + keyword = query[0:word_len] + if word_len == 12 and keyword.lower() == "concurrently": + return False + return True + + if word_len == 5 and keyword.lower() == "alter": + query = query[word_len:query_len] + query = query.strip() + query_len = len(query) + word_len = 0 + + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + keyword = query[0:word_len] + + # ALTER SYSTEM isn't allowed in xacts + if word_len == 6 and keyword.lower() == "system": + return False + return True + + # Note: these tests will match DROP SYSTEM and REINDEX TABLESPACE, which + # aren't really valid commands so we don't care much. The other four + # possible matches are correct. + if word_len == 4 and keyword.lower() == "drop" \ + or word_len == 7 and keyword.lower() == "reindex": + query = query[word_len:query_len] + query = query.strip() + query_len = len(query) + word_len = 0 + + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + keyword = query[0:word_len] + if word_len == 8 and keyword.lower() == "database": + return False + if word_len == 6 and keyword.lower() == "system": + return False + if word_len == 10 and keyword.lower() == "tablespace": + return False + return True + + # DISCARD ALL isn't allowed in xacts, but other variants are allowed. + if word_len == 7 and keyword.lower() == "discard": + query = query[word_len:query_len] + query = query.strip() + query_len = len(query) + word_len = 0 + + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + keyword = query[0:word_len] + if word_len == 3 and keyword.lower() == "all": + return False + return True + + return True diff --git a/web/pgadmin/tools/sqleditor/utils/start_running_query.py b/web/pgadmin/tools/sqleditor/utils/start_running_query.py new file mode 100644 index 00000000..f5e1feae --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/start_running_query.py @@ -0,0 +1,172 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Start executing the query in async mode.""" + +import pickle +import random + +from flask import Response +from flask_babel import gettext + +from config import PG_DEFAULT_DRIVER +from pgadmin.tools.sqleditor.utils.apply_explain_plan_wrapper import apply_explain_plan_wrapper_if_needed +from pgadmin.tools.sqleditor.utils.update_session_grid_transaction import update_session_grid_transaction +from pgadmin.tools.sqleditor.utils.is_begin_required import is_begin_required +from pgadmin.tools.sqleditor.utils.constant_definition import TX_STATUS_IDLE, TX_STATUS_INERROR + +from pgadmin.utils.ajax import make_json_response, internal_server_error +from pgadmin.utils.driver import get_driver +from pgadmin.utils.exception import ConnectionLost + + +class StartRunningQuery: + + def __init__(self, blueprint_object, logger): + self.http_session = None + self.blueprint_object = blueprint_object + self.connection_id = str(random.randint(1, 9999999)) + self.logger = logger + + def execute(self, sql, trans_id, http_session): + session_obj = StartRunningQuery.retrieve_session_information(http_session, trans_id) + if type(session_obj) is Response: + return session_obj + + transaction_object = pickle.loads(session_obj['command_obj']) + can_edit = False + can_filter = False + if transaction_object is not None and session_obj is not None: + # set fetched row count to 0 as we are executing query again. + transaction_object.update_fetched_row_cnt(0) + self.__retrieve_connection_id(transaction_object) + + try: + manager = get_driver( + PG_DEFAULT_DRIVER).connection_manager(transaction_object.sid) + conn = manager.connection(did=transaction_object.did, conn_id=self.connection_id, + auto_reconnect=False, + use_binary_placeholder=True, + array_to_string=True) + except ConnectionLost: + raise + except Exception as e: + self.logger.error(e) + return internal_server_error(errormsg=str(e)) + + # Connect to the Server if not connected. + if not conn.connected(): + status, msg = conn.connect() + if not status: + self.logger.error(msg) + return internal_server_error(errormsg=str(msg)) + + effective_sql_statement = apply_explain_plan_wrapper_if_needed(manager, sql) + + result, status = self.__execute_query( + conn, + session_obj, + effective_sql_statement, + trans_id, + transaction_object + ) + + can_edit = transaction_object.can_edit() + can_filter = transaction_object.can_filter() + + else: + status = False + result = gettext( + 'Either transaction object or session object not found.') + return make_json_response( + data={ + 'status': status, 'result': result, + 'can_edit': can_edit, 'can_filter': can_filter, + 'info_notifier_timeout': self.blueprint_object.info_notifier_timeout.get() + } + ) + + def __retrieve_connection_id(self, trans_obj): + conn_id = trans_obj.conn_id + # if conn_id is None then we will have to create a new connection + if conn_id is not None: + self.connection_id = conn_id + + def __execute_query(self, conn, session_obj, sql, trans_id, trans_obj): + if conn.connected(): + # on successful connection set the connection id to the + # transaction object + trans_obj.set_connection_id(self.connection_id) + + StartRunningQuery.save_transaction_in_session(session_obj, trans_id, trans_obj) + + # If auto commit is False and transaction status is Idle + # then call is_begin_not_required() function to check BEGIN + # is required or not. + + if StartRunningQuery.is_begin_required_for_sql_query(trans_obj, conn, sql): + conn.execute_void("BEGIN;") + + # Execute sql asynchronously with params is None + # and formatted_error is True. + try: + status, result = conn.execute_async(sql) + except ConnectionLost: + raise + + # If the transaction aborted for some reason and + # Auto RollBack is True then issue a rollback to cleanup. + if StartRunningQuery.is_rollback_statement_required(trans_obj, conn): + conn.execute_void("ROLLBACK;") + else: + status = False + result = gettext( + 'Not connected to server or connection with the server has ' + 'been closed.') + return result, status + + @staticmethod + def is_begin_required_for_sql_query(trans_obj, conn, sql): + return not trans_obj.auto_commit \ + and conn.transaction_status() == TX_STATUS_IDLE \ + and is_begin_required(sql) + + @staticmethod + def is_rollback_statement_required(trans_obj, conn): + return conn.transaction_status() == TX_STATUS_INERROR and trans_obj.auto_rollback + + @staticmethod + def save_transaction_in_session(session, transaction_id, transaction): + # As we changed the transaction object we need to + # restore it and update the session variable. + session['command_obj'] = pickle.dumps(transaction, -1) + update_session_grid_transaction(transaction_id, session) + + @staticmethod + def retrieve_session_information(http_session, transaction_id): + if 'gridData' not in http_session: + return make_json_response( + success=0, + errormsg=gettext('Transaction ID not found in the session.'), + info='DATAGRID_TRANSACTION_REQUIRED', status=404 + ) + grid_data = http_session['gridData'] + # Return from the function if transaction id not found + if str(transaction_id) not in grid_data: + return make_json_response( + success=0, + errormsg=gettext('Transaction ID not found in the session.'), + info='DATAGRID_TRANSACTION_REQUIRED', + status=404 + ) + # Fetch the object for the specified transaction id. + # Use pickle.loads function to get the command object + return grid_data[str(transaction_id)] + + diff --git a/web/pgadmin/tools/sqleditor/utils/tests/__init__.py b/web/pgadmin/tools/sqleditor/utils/tests/__init__.py new file mode 100644 index 00000000..590026ad --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/tests/__init__.py @@ -0,0 +1,8 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## diff --git a/web/pgadmin/tools/sqleditor/utils/tests/test_apply_explain_plan_wrapper.py b/web/pgadmin/tools/sqleditor/utils/tests/test_apply_explain_plan_wrapper.py new file mode 100644 index 00000000..6cf320eb --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/tests/test_apply_explain_plan_wrapper.py @@ -0,0 +1,121 @@ +####################################################################### +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Apply Explain plan wrapper to sql object.""" +import sys + +from pgadmin.tools.sqleditor.utils import apply_explain_plan_wrapper_if_needed +from pgadmin.utils.route import BaseTestGenerator + +if sys.version_info < (3, 3): + from mock import patch, MagicMock +else: + from unittest.mock import patch, MagicMock + + +class StartRunningQueryTest(BaseTestGenerator): + """ + Check that the apply_explain_plan_weapper_if_needed method works as intended + """ + scenarios = [ + ('When explain_plan is none, it should return unaltered SQL', dict( + function_input_parameters={ + 'manager': MagicMock(), + 'sql': { + 'sql': 'some sql', + 'explain_plan': None + } + }, + + expect_render_template_mock_parameters=None, + + expected_return_value='some sql' + )), + ('When explain_plan is not present, it should return unaltered SQL', dict( + function_input_parameters={ + 'manager': MagicMock(), + 'sql': { + 'sql': 'some sql' + } + }, + + expect_render_template_mock_parameters=None, + + expected_return_value='some sql' + )), + ('When explain_plan is present for a Postgres server version 10, it should return SQL with explain plan', dict( + function_input_parameters={ + 'manager': MagicMock(version=10, server_type='pg'), + 'sql': { + 'sql': 'some sql', + 'explain_plan': { + 'format': 'json', + 'analyze': False, + 'verbose': True, + 'buffers': False, + 'timing': True + } + } + }, + + expect_render_template_mock_parameters=dict( + template_name_or_list='sqleditor/sql/#10#/explain_plan.sql', + named_parameters=dict( + format='json', + analyze=False, + verbose=True, + buffers=False, + timing=True + )), + + expected_return_value='EXPLAIN (FORMAT JSON, ANALYZE FALSE, VERBOSE TRUE, COSTS FALSE, BUFFERS FALSE, ' + 'TIMING TRUE) some sql' + )), + ('When explain_plan is present for a GreenPlum server version 5, it should return SQL with explain plan', dict( + function_input_parameters={ + 'manager': MagicMock(version=80323, server_type='gpdb'), + 'sql': { + 'sql': 'some sql', + 'explain_plan': { + 'format': 'json', + 'analyze': False, + 'verbose': True, + 'buffers': False, + 'timing': True + } + } + }, + + expect_render_template_mock_parameters=dict( + template_name_or_list='sqleditor/sql/#gpdb#80323#/explain_plan.sql', + named_parameters=dict( + format='json', + analyze=False, + verbose=True, + buffers=False, + timing=True + )), + + expected_return_value='EXPLAIN some sql' + )) + ] + + def runTest(self): + with patch('pgadmin.tools.sqleditor.utils.apply_explain_plan_wrapper.render_template') as render_template_mock: + render_template_mock.return_value = self.expected_return_value + result = apply_explain_plan_wrapper_if_needed(**self.function_input_parameters) + self.assertEquals(result, self.expected_return_value) + if self.expect_render_template_mock_parameters: + render_template_mock.assert_called_with( + self.expect_render_template_mock_parameters['template_name_or_list'], + sql=self.function_input_parameters['sql']['sql'], + **self.expect_render_template_mock_parameters['named_parameters'] + ) + else: + render_template_mock.assert_not_called() diff --git a/web/pgadmin/tools/sqleditor/utils/tests/test_start_running_query.py b/web/pgadmin/tools/sqleditor/utils/tests/test_start_running_query.py new file mode 100644 index 00000000..d53c0248 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/tests/test_start_running_query.py @@ -0,0 +1,445 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +import sys + +from flask import Response +import simplejson as json + +from pgadmin.tools.sqleditor.utils.start_running_query import StartRunningQuery +from pgadmin.utils.exception import ConnectionLost +from pgadmin.utils.route import BaseTestGenerator + +if sys.version_info < (3, 3): + from mock import patch, MagicMock +else: + from unittest.mock import patch, MagicMock + +get_driver_exception = Exception('get_driver exception') + + +class StartRunningQueryTest(BaseTestGenerator): + """ + Check that the start_running_query method works as intended + """ + + scenarios = [ + ('When gridData is not present in the session, it returns an error', dict( + function_parameters=dict( + sql=dict(sql='some sql', explain_plan=None), + trans_id=123, + http_session=dict() + ), + pickle_load_return=None, + get_driver_exception=False, + manager_connection_exception=None, + + is_connected_to_server=False, + connection_connect_return=None, + execute_async_return_value=None, + is_begin_required=False, + is_rollback_required=False, + apply_explain_plan_wrapper_if_needed_return_value='some sql', + + expect_make_json_response_to_have_been_called_with=dict( + success=0, + errormsg='Transaction ID not found in the session.', + info='DATAGRID_TRANSACTION_REQUIRED', + status=404, + ), + expect_internal_server_error_to_have_been_called_with=None, + expected_logger_error=None, + expect_execute_void_called_with='some sql', + )), + ('When transactionId is not present in the gridData, it returns an error', dict( + function_parameters=dict( + sql=dict(sql='some sql', explain_plan=None), + trans_id=123, + http_session=dict(gridData=dict()) + ), + pickle_load_return=None, + get_driver_exception=False, + manager_connection_exception=None, + + is_connected_to_server=False, + connection_connect_return=None, + execute_async_return_value=None, + is_begin_required=False, + is_rollback_required=False, + apply_explain_plan_wrapper_if_needed_return_value='some sql', + + expect_make_json_response_to_have_been_called_with=dict( + success=0, + errormsg='Transaction ID not found in the session.', + info='DATAGRID_TRANSACTION_REQUIRED', + status=404, + ), + expect_internal_server_error_to_have_been_called_with=None, + expected_logger_error=None, + expect_execute_void_called_with='some sql', + )), + ('When the command information for the transaction cannot be retrieved, it returns an error', dict( + function_parameters=dict( + sql=dict(sql='some sql', explain_plan=None), + trans_id=123, + http_session=dict(gridData={'123': dict(command_obj='')}) + ), + pickle_load_return=None, + get_driver_exception=False, + manager_connection_exception=None, + + is_connected_to_server=False, + connection_connect_return=None, + execute_async_return_value=None, + is_begin_required=False, + is_rollback_required=False, + apply_explain_plan_wrapper_if_needed_return_value='some sql', + + expect_make_json_response_to_have_been_called_with=dict( + data=dict( + status=False, + result='Either transaction object or session object not found.', + can_edit=False, + can_filter=False, + info_notifier_timeout=5 + ) + ), + expect_internal_server_error_to_have_been_called_with=None, + expected_logger_error=None, + expect_execute_void_called_with='some sql', + )), + ('When exception happens while retrieving the database driver, it returns an error', dict( + function_parameters=dict( + sql=dict(sql='some sql', explain_plan=None), + trans_id=123, + http_session=dict(gridData={'123': dict(command_obj='')}) + ), + pickle_load_return=MagicMock(conn_id=1, update_fetched_row_cnt=MagicMock()), + get_driver_exception=True, + manager_connection_exception=None, + + is_connected_to_server=False, + connection_connect_return=None, + execute_async_return_value=None, + is_begin_required=False, + is_rollback_required=False, + apply_explain_plan_wrapper_if_needed_return_value='some sql', + + expect_make_json_response_to_have_been_called_with=None, + expect_internal_server_error_to_have_been_called_with=dict( + errormsg='get_driver exception' + ), + expected_logger_error=get_driver_exception, + expect_execute_void_called_with='some sql', + )), + ('When ConnectionLost happens while retrieving the database connection, it returns an error', dict( + function_parameters=dict( + sql=dict(sql='some sql', explain_plan=None), + trans_id=123, + http_session=dict(gridData={'123': dict(command_obj='')}) + ), + pickle_load_return=MagicMock(conn_id=1, update_fetched_row_cnt=MagicMock()), + get_driver_exception=False, + manager_connection_exception=ConnectionLost('1', '2', '3'), + + is_connected_to_server=False, + connection_connect_return=None, + execute_async_return_value=None, + is_begin_required=False, + is_rollback_required=False, + apply_explain_plan_wrapper_if_needed_return_value='some sql', + + expect_make_json_response_to_have_been_called_with=None, + expect_internal_server_error_to_have_been_called_with=None, + expected_logger_error=None, + expect_execute_void_called_with='some sql', + )), + ('When is not connected to the server and fails to connect, it returns an error', dict( + function_parameters=dict( + sql=dict(sql='some sql', explain_plan=None), + trans_id=123, + http_session=dict(gridData={'123': dict(command_obj='')}) + ), + pickle_load_return=MagicMock(conn_id=1, update_fetched_row_cnt=MagicMock()), + get_driver_exception=False, + manager_connection_exception=None, + + is_connected_to_server=False, + connection_connect_return=[False, 'Unable to connect to server'], + execute_async_return_value=None, + is_begin_required=False, + is_rollback_required=False, + apply_explain_plan_wrapper_if_needed_return_value='some sql', + + expect_make_json_response_to_have_been_called_with=None, + expect_internal_server_error_to_have_been_called_with=dict( + errormsg='Unable to connect to server' + ), + expected_logger_error='Unable to connect to server', + expect_execute_void_called_with='some sql', + )), + ('When server is connected and start query async request, it returns an success message', dict( + function_parameters=dict( + sql=dict(sql='some sql', explain_plan=None), + trans_id=123, + http_session=dict(gridData={'123': dict(command_obj='')}) + ), + pickle_load_return=MagicMock( + conn_id=1, + update_fetched_row_cnt=MagicMock(), + set_connection_id=MagicMock(), + auto_commit=True, + auto_rollback=False, + can_edit=lambda: True, + can_filter=lambda: True + ), + get_driver_exception=False, + manager_connection_exception=None, + + is_connected_to_server=True, + connection_connect_return=None, + execute_async_return_value=[True, 'async function result output'], + is_begin_required=False, + is_rollback_required=False, + apply_explain_plan_wrapper_if_needed_return_value='some sql', + + expect_make_json_response_to_have_been_called_with=dict( + data=dict( + status=True, + result='async function result output', + can_edit=True, + can_filter=True, + info_notifier_timeout=5 + ) + ), + expect_internal_server_error_to_have_been_called_with=None, + expected_logger_error=None, + expect_execute_void_called_with='some sql', + )), + ('When server is connected and start query async request and begin is required, ' + 'it returns an success message', dict( + function_parameters=dict( + sql=dict(sql='some sql', explain_plan=None), + trans_id=123, + http_session=dict(gridData={'123': dict(command_obj='')}) + ), + pickle_load_return=MagicMock( + conn_id=1, + update_fetched_row_cnt=MagicMock(), + set_connection_id=MagicMock(), + auto_commit=True, + auto_rollback=False, + can_edit=lambda: True, + can_filter=lambda: True + ), + get_driver_exception=False, + manager_connection_exception=None, + + is_connected_to_server=True, + connection_connect_return=None, + execute_async_return_value=[True, 'async function result output'], + is_begin_required=True, + is_rollback_required=False, + apply_explain_plan_wrapper_if_needed_return_value='some sql', + + expect_make_json_response_to_have_been_called_with=dict( + data=dict( + status=True, + result='async function result output', + can_edit=True, + can_filter=True, + info_notifier_timeout=5 + ) + ), + expect_internal_server_error_to_have_been_called_with=None, + expected_logger_error=None, + expect_execute_void_called_with='some sql', + )), + ('When server is connected and start query async request and rollback is required, ' + 'it returns an success message', dict( + function_parameters=dict( + sql=dict(sql='some sql', explain_plan=None), + trans_id=123, + http_session=dict(gridData={'123': dict(command_obj='')}) + ), + pickle_load_return=MagicMock( + conn_id=1, + update_fetched_row_cnt=MagicMock(), + set_connection_id=MagicMock(), + auto_commit=True, + auto_rollback=False, + can_edit=lambda: True, + can_filter=lambda: True + ), + get_driver_exception=False, + manager_connection_exception=None, + + is_connected_to_server=True, + connection_connect_return=None, + execute_async_return_value=[True, 'async function result output'], + is_begin_required=False, + is_rollback_required=True, + apply_explain_plan_wrapper_if_needed_return_value='some sql', + + expect_make_json_response_to_have_been_called_with=dict( + data=dict( + status=True, + result='async function result output', + can_edit=True, + can_filter=True, + info_notifier_timeout=5 + ) + ), + expect_internal_server_error_to_have_been_called_with=None, + expected_logger_error=None, + expect_execute_void_called_with='some sql', + )), + ('When server is connected and start query async request with explain plan wrapper, ' + 'it returns an success message', dict( + function_parameters=dict( + sql=dict(sql='some sql', explain_plan=None), + trans_id=123, + http_session=dict(gridData={'123': dict(command_obj='')}) + ), + pickle_load_return=MagicMock( + conn_id=1, + update_fetched_row_cnt=MagicMock(), + set_connection_id=MagicMock(), + auto_commit=True, + auto_rollback=False, + can_edit=lambda: True, + can_filter=lambda: True + ), + get_driver_exception=False, + manager_connection_exception=None, + + is_connected_to_server=True, + connection_connect_return=None, + execute_async_return_value=[True, 'async function result output'], + is_begin_required=False, + is_rollback_required=True, + apply_explain_plan_wrapper_if_needed_return_value='EXPLAIN PLAN some sql', + + expect_make_json_response_to_have_been_called_with=dict( + data=dict( + status=True, + result='async function result output', + can_edit=True, + can_filter=True, + info_notifier_timeout=5 + ) + ), + expect_internal_server_error_to_have_been_called_with=None, + expected_logger_error=None, + expect_execute_void_called_with='EXPLAIN PLAN some sql', + )), + ] + + @patch('pgadmin.tools.sqleditor.utils.start_running_query.apply_explain_plan_wrapper_if_needed') + @patch('pgadmin.tools.sqleditor.utils.start_running_query.make_json_response') + @patch('pgadmin.tools.sqleditor.utils.start_running_query.pickle') + @patch('pgadmin.tools.sqleditor.utils.start_running_query.get_driver') + @patch('pgadmin.tools.sqleditor.utils.start_running_query.internal_server_error') + @patch('pgadmin.tools.sqleditor.utils.start_running_query.update_session_grid_transaction') + def runTest(self, update_session_grid_transaction_mock, + internal_server_error_mock, get_driver_mock, pickle_mock, + make_json_response_mock, apply_explain_plan_wrapper_if_needed_mock): + """Check correct function is called to handle to run query.""" + self.connection = None + + self.loggerMock = MagicMock(error=MagicMock()) + expected_response = Response(response=json.dumps({'errormsg': 'some value'})) + make_json_response_mock.return_value = expected_response + if self.expect_internal_server_error_to_have_been_called_with is not None: + internal_server_error_mock.return_value = expected_response + pickle_mock.loads.return_value = self.pickle_load_return + blueprint_mock = MagicMock(info_notifier_timeout=MagicMock(get=lambda: 5)) + + if self.is_begin_required: + StartRunningQuery.is_begin_required_for_sql_query = MagicMock(return_value=True) + else: + StartRunningQuery.is_begin_required_for_sql_query = MagicMock(return_value=False) + if self.is_rollback_required: + StartRunningQuery.is_rollback_statement_required = MagicMock(return_value=True) + else: + StartRunningQuery.is_rollback_statement_required = MagicMock(return_value=False) + + apply_explain_plan_wrapper_if_needed_mock.return_value = self.apply_explain_plan_wrapper_if_needed_return_value + + manager = self.__create_manager() + if self.get_driver_exception: + get_driver_mock.side_effect = get_driver_exception + else: + get_driver_mock.return_value = MagicMock(connection_manager=lambda session_id: manager) + + try: + result = StartRunningQuery( + blueprint_mock, + self.loggerMock + ).execute( + **self.function_parameters + ) + if self.manager_connection_exception is not None: + self.fail('Exception: "' + str(self.manager_connection_exception) + '" excepted but not raised') + + self.assertEquals(result, expected_response) + + except AssertionError: + raise + except Exception as exception: + self.assertEquals(self.manager_connection_exception, exception) + + self.__mock_assertions(internal_server_error_mock, make_json_response_mock) + if self.is_connected_to_server: + apply_explain_plan_wrapper_if_needed_mock.assert_called_with(manager, self.function_parameters['sql']) + + def __create_manager(self): + self.connection = MagicMock( + connected=lambda: self.is_connected_to_server, + connect=MagicMock(), + execute_async=MagicMock(), + execute_void=MagicMock(), + ) + self.connection.connect.return_value = self.connection_connect_return + self.connection.execute_async.return_value = self.execute_async_return_value + if self.manager_connection_exception is None: + manager = MagicMock( + connection=lambda did, conn_id, use_binary_placeholder, array_to_string, auto_reconnect: self.connection + ) + else: + manager = MagicMock() + manager.connection.side_effect = self.manager_connection_exception + return manager + + def __mock_assertions(self, internal_server_error_mock, make_json_response_mock): + if self.expect_make_json_response_to_have_been_called_with is not None: + make_json_response_mock.assert_called_with(**self.expect_make_json_response_to_have_been_called_with) + else: + make_json_response_mock.assert_not_called() + if self.expect_internal_server_error_to_have_been_called_with is not None: + internal_server_error_mock.assert_called_with(**self.expect_internal_server_error_to_have_been_called_with) + else: + internal_server_error_mock.assert_not_called() + if self.execute_async_return_value is not None: + self.connection.execute_async.assert_called_with(self.expect_execute_void_called_with) + else: + self.connection.execute_async.assert_not_called() + + if self.expected_logger_error is not None: + self.loggerMock.error.assert_called_with(self.expected_logger_error) + else: + self.loggerMock.error.assert_not_called() + + if self.is_begin_required: + self.connection.execute_void.assert_called_with('BEGIN;') + elif not self.is_rollback_required: + self.connection.execute_void.assert_not_called() + if self.is_rollback_required: + self.connection.execute_void.assert_called_with('ROLLBACK;') + elif not self.is_begin_required: + self.connection.execute_void.assert_not_called() diff --git a/web/pgadmin/tools/sqleditor/utils/update_session_grid_transaction.py b/web/pgadmin/tools/sqleditor/utils/update_session_grid_transaction.py new file mode 100644 index 00000000..aef78f23 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/update_session_grid_transaction.py @@ -0,0 +1,18 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Update session with gridData.""" +from flask import session + + +def update_session_grid_transaction(trans_id, data): + if 'gridData' in session: + grid_data = session['gridData'] + grid_data[str(trans_id)] = data + session['gridData'] = grid_data diff --git a/web/pgadmin/utils/compile_template_name.py b/web/pgadmin/utils/compile_template_name.py new file mode 100644 index 00000000..6eb2431f --- /dev/null +++ b/web/pgadmin/utils/compile_template_name.py @@ -0,0 +1,17 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +import os + + +def compile_template_name(template_prefix, template_file_name, server_type, version): + if server_type == 'gpdb': + version_path = '#{0}#{1}#'.format(server_type, version) + else: + version_path = '#{0}#'.format(version) + return os.path.join(template_prefix, version_path, template_file_name) diff --git a/web/pgadmin/utils/tests/test_compile_template_name.py b/web/pgadmin/utils/tests/test_compile_template_name.py new file mode 100644 index 00000000..97f1b05c --- /dev/null +++ b/web/pgadmin/utils/tests/test_compile_template_name.py @@ -0,0 +1,34 @@ +####################################################################### +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +from pgadmin.utils.compile_template_name import compile_template_name +from pgadmin.utils.route import BaseTestGenerator + + +class StartRunningQueryTest(BaseTestGenerator): + """ + Check that the apply_explain_plan_weapper_if_needed method works as intended + """ + scenarios = [ + ('When server is Postgres and version is 10, it returns the path to the postgres template', dict( + server_type='pg', + version=100000, + + expected_return_value='some/prefix/#100000#/some_file.sql' + )), + ('When server is GreenPlum and version is 5, it returns the path to the GreenPlum template', dict( + server_type='gpdb', + version=80323, + + expected_return_value='some/prefix/#gpdb#80323#/some_file.sql' + )), + ] + + def runTest(self): + result = compile_template_name('some/prefix', 'some_file.sql', self.server_type, self.version) + self.assertEquals(result, self.expected_return_value) diff --git a/web/pgadmin/utils/versioned_template_loader.py b/web/pgadmin/utils/versioned_template_loader.py index 19693012..8e8259fe 100644 --- a/web/pgadmin/utils/versioned_template_loader.py +++ b/web/pgadmin/utils/versioned_template_loader.py @@ -6,7 +6,6 @@ # This software is released under the PostgreSQL Licence # ########################################################################## - from flask.templating import DispatchingJinjaLoader from jinja2 import TemplateNotFound @@ -54,6 +53,7 @@ class VersionedTemplateLoader(DispatchingJinjaLoader): template_path = path_start + '/' + \ server_version['name'] + '/' + file_name + try: return super(VersionedTemplateLoader, self).get_source( environment, template_path diff --git a/web/regression/javascript/fake_endpoints.js b/web/regression/javascript/fake_endpoints.js index 0250a0df..63ab05dc 100644 --- a/web/regression/javascript/fake_endpoints.js +++ b/web/regression/javascript/fake_endpoints.js @@ -8,5 +8,9 @@ ////////////////////////////////////////////////////////////////////////// define(function () { - return {'static': '/base/pgadmin/static/'}; + return { + 'static': '/base/pgadmin/static/', + 'sqleditor.poll': '/sqleditor/query_tool/poll/', + 'sqleditor.query_tool_start': '/sqleditor/query_tool/start/' + }; }); diff --git a/web/regression/javascript/sqleditor/execute_query_spec.js b/web/regression/javascript/sqleditor/execute_query_spec.js new file mode 100644 index 00000000..98faed7d --- /dev/null +++ b/web/regression/javascript/sqleditor/execute_query_spec.js @@ -0,0 +1,1702 @@ +////////////////////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////////////////// + +import * as subject from 'sources/sqleditor/execute_query'; +import * as transaction from 'sources/sqleditor/is_new_transaction_required'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import $ from 'jquery'; + +const context = describe; + +describe('ExecuteQuery', () => { + let sqlEditorMock; + let networkMock; + let executeQuery; + let userManagementMock; + let isNewTransactionRequiredMock; + + const startTime = new Date(2018, 1, 29, 12, 15, 52); + beforeEach(() => { + networkMock = new MockAdapter(axios); + jasmine.addMatchers({jQuerytoHaveBeenCalledWith: jQuerytoHaveBeenCalledWith}); + userManagementMock = jasmine.createSpyObj('UserManagement', [ + 'is_pga_login_required', + 'pga_login', + ]); + + sqlEditorMock = jasmine.createSpyObj('SqlEditor', [ + 'call_render_after_poll', + 'disable_tool_buttons', + 'resetQueryHistoryObject', + 'setIsQueryRunning', + 'trigger', + 'update_msg_history', + '_highlight_error', + '_init_polling_flags', + 'save_state', + 'init_transaction', + ]); + sqlEditorMock.transId = 123; + sqlEditorMock.rows_affected = 1000; + executeQuery = new subject.ExecuteQuery(sqlEditorMock, userManagementMock); + isNewTransactionRequiredMock = spyOn(transaction, 'is_new_transaction_required'); + }); + + describe('#poll', () => { + let cancelButtonSpy; + let response; + + beforeEach(() => { + sqlEditorMock.POLL_FALLBACK_TIME = () => { + return 0; + }; + + cancelButtonSpy = spyOn($.fn, 'prop'); + executeQuery.delayedPoll = jasmine.createSpy('ExecuteQuery.delayedPoll'); + }); + + afterEach(() => { + }); + + context('when SQLEditor is the query tool', () => { + beforeEach(() => { + sqlEditorMock.is_query_tool = true; + }); + + describe('when server answer with success', () => { + describe('when query was successful', () => { + beforeEach(() => { + response = { + data: {status: 'Success'}, + }; + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(200, response); + + executeQuery.poll(); + }); + + it('should update the loading icon message', (done) => { + setTimeout(() => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith( + 'pgadmin-sqleditor:loading-icon:message', + 'Loading data from the database server and rendering...' + ); + done(); + }, 0); + }); + + it('should render the results', (done) => { + setTimeout(() => { + expect(sqlEditorMock.call_render_after_poll) + .toHaveBeenCalledWith({status: 'Success'}); + done(); + }, 0); + }); + }); + + describe('when query is still running', () => { + context('when no additional information is returned', () => { + beforeEach(() => { + response = { + data: {status: 'Busy'}, + }; + + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(200, response); + executeQuery.poll(); + }); + + it('should set the flag to inform SQLEditor a query is running', (done) => { + setTimeout(() => { + expect(sqlEditorMock.setIsQueryRunning) + .toHaveBeenCalledWith(true); + done(); + }, 0); + }); + + it('should does not update history', (done) => { + setTimeout(() => { + expect(sqlEditorMock.update_msg_history).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should recursively call polling', (done) => { + setTimeout(() => { + expect(executeQuery.delayedPoll) + .toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + context('when additional information is returned', () => { + beforeEach(() => { + response = { + data: { + status: 'Busy', + result: 'Some important result', + }, + }; + + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(200, response); + executeQuery.poll(); + }); + + it('should set the flag to inform SQLEditor a query is running', (done) => { + setTimeout(() => { + expect(sqlEditorMock.setIsQueryRunning) + .toHaveBeenCalledWith(true); + done(); + }, 0); + }); + + it('should update history message', (done) => { + setTimeout(() => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith('Busy', 'Some important result', false); + done(); + }, 0); + }); + + it('should recursively call polling', (done) => { + setTimeout(() => { + expect(executeQuery.delayedPoll) + .toHaveBeenCalled(); + done(); + }, 0); + }); + }); + }); + + describe('when the application lost connection with the database', () => { + beforeEach(() => { + response = { + data: { + status: 'NotConnected', + result: 'Some interesting result', + }, + }; + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(200, response); + + executeQuery.poll(); + }); + + it('should hide the loading icon', (done) => { + setTimeout(() => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should add new entry to history and update the Messages tab and clear the result grid', (done) => { + setTimeout(() => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith( + false, + 'Some interesting result', + true + ); + done(); + }, 0); + }); + + it('should enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons) + .toHaveBeenCalledWith(false); + done(); + }, 0); + }); + + it('should disable the cancel button', (done) => { + setTimeout( + () => { + expect(cancelButtonSpy) + .toHaveBeenCalledWith('disabled', true); + done(); + }, 0); + }); + }); + + describe('when query was cancelled', () => { + beforeEach(() => { + response = { + data: {status: 'Cancel'}, + }; + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(200, response); + + executeQuery.poll(); + }); + + it('should hide the loading icon', (done) => { + setTimeout(() => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should add new entry to history, add cancellation message to Messages tab and clear the result grid', (done) => { + setTimeout(() => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith( + false, + 'Execution Cancelled!', + true + ); + done(); + }, 0); + }); + }); + }); + + describe('when an error occur', () => { + let errorMessageJson = { + errormsg: 'Some error in JSON', + }; + let errorMessageText = 'Some plain text error'; + + describe('when the connection to the server was lost', () => { + describe('when JSON response is available', () => { + describe('when login is not required', () => { + beforeEach(() => { + userManagementMock.is_pga_login_required.and.returnValue(false); + response = {responseJSON: errorMessageJson}; + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(401, response); + + executeQuery.poll(); + }); + + it('should hide the loading icon', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should reset last query information', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.resetQueryHistoryObject) + .toHaveBeenCalledWith(sqlEditorMock); + done(); + }, 0); + }); + + it('should highlight the error in the SQL panel', (done) => { + setTimeout( + () => { + expect(sqlEditorMock._highlight_error) + .toHaveBeenCalledWith('Some error in JSON'); + done(); + }, 0); + }); + + it('should add new entry to history and update the Messages tab', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith(false, 'Some error in JSON'); + done(); + }, 0); + }); + + it('should enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons) + .toHaveBeenCalledWith(false); + done(); + }, 0); + }); + + it('should disable the cancel button', (done) => { + setTimeout( + () => { + expect(cancelButtonSpy) + .toHaveBeenCalledWith('disabled', true); + done(); + }, 0); + }); + + it('should not login is displayed', (done) => { + setTimeout( + () => { + expect(userManagementMock.pga_login).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('when login is required', () => { + beforeEach(() => { + userManagementMock.is_pga_login_required.and.returnValue(true); + response = {responseJSON: errorMessageJson}; + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(401, response); + + executeQuery.poll(); + }); + + it('should hide the loading icon', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should reset last query information', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.resetQueryHistoryObject) + .toHaveBeenCalledWith(sqlEditorMock); + done(); + }, 0); + }); + + it('should not highlight the error in the SQL panel', (done) => { + setTimeout( + () => { + expect(sqlEditorMock._highlight_error).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should not add new entry to history and update the Messages tab', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.update_msg_history).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons) + .toHaveBeenCalledWith(false); + done(); + }, 0); + }); + + it('should disable the cancel button', (done) => { + setTimeout( + () => { + expect(cancelButtonSpy) + .toHaveBeenCalledWith('disabled', true); + done(); + }, 0); + }); + + it('should login is displayed', (done) => { + setTimeout( + () => { + expect(userManagementMock.pga_login) + .toHaveBeenCalled(); + done(); + }, 0); + }); + }); + }); + + describe('when no JSON response is available', () => { + describe('when login is not required', () => { + beforeEach(() => { + userManagementMock.is_pga_login_required.and.returnValue(false); + response = { + errormsg: errorMessageText, + }; + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(401, response); + + executeQuery.poll(); + }); + + it('should hide the loading icon', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should reset last query information', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.resetQueryHistoryObject) + .toHaveBeenCalledWith(sqlEditorMock); + done(); + }, 0); + }); + + it('should highlight the error in the SQL panel', (done) => { + setTimeout( + () => { + expect(sqlEditorMock._highlight_error) + .toHaveBeenCalledWith('Some plain text error'); + done(); + }, 0); + }); + + it('should add new entry to history and update the Messages tab', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith(false, 'Some plain text error'); + done(); + }, 0); + }); + + it('should enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons) + .toHaveBeenCalledWith(false); + done(); + }, 0); + }); + + it('should disable the cancel button', (done) => { + setTimeout( + () => { + expect(cancelButtonSpy) + .toHaveBeenCalledWith('disabled', true); + done(); + }, 0); + }); + + it('should login is not displayed', (done) => { + setTimeout( + () => { + expect(userManagementMock.pga_login).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('when login is required', () => { + beforeEach(() => { + userManagementMock.is_pga_login_required.and.returnValue(true); + response = { + errormsg: errorMessageText, + }; + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(401, response); + + executeQuery.poll(); + }); + + it('should hide the loading icon', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should reset last query information', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.resetQueryHistoryObject) + .toHaveBeenCalledWith(sqlEditorMock); + done(); + }, 0); + }); + + it('should not highlight the error in the SQL panel', (done) => { + setTimeout( + () => { + expect(sqlEditorMock._highlight_error).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should not add new entry to history and update the Messages tab', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.update_msg_history).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons) + .toHaveBeenCalledWith(false); + done(); + }, 0); + }); + + it('should disable the cancel button', (done) => { + setTimeout( + () => { + expect(cancelButtonSpy) + .toHaveBeenCalledWith('disabled', true); + done(); + }, 0); + }); + + it('should login is displayed', (done) => { + setTimeout( + () => { + expect(userManagementMock.pga_login) + .toHaveBeenCalled(); + done(); + }, 0); + }); + }); + }); + + describe('when cannot reach the Python Server', () => { + beforeEach(() => { + response = {readyState: 0}; + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(401, response); + + executeQuery.poll(); + }); + + it('should hide the loading icon', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should reset last query information', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.resetQueryHistoryObject) + .toHaveBeenCalledWith(sqlEditorMock); + done(); + }, 0); + }); + + it('should not highlight the error in the SQL panel', (done) => { + setTimeout( + () => { + expect(sqlEditorMock._highlight_error).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should add new entry to history and update the Messages tab', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith(false, 'Not connected to the server or the connection to the server has been closed.'); + done(); + }, 0); + }); + + it('should enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons) + .toHaveBeenCalledWith(false); + done(); + }, 0); + }); + + it('should disable the cancel button', (done) => { + setTimeout( + () => { + expect(cancelButtonSpy) + .toHaveBeenCalledWith('disabled', true); + done(); + }, 0); + }); + + it('should login is not displayed', (done) => { + setTimeout( + () => { + expect(userManagementMock.pga_login).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + }); + }); + }); + }); + + context('when SQLEditor is NOT the query tool', () => { + beforeEach(() => { + sqlEditorMock.is_query_tool = false; + }); + + describe('when server answer with success', () => { + describe('when query was successful', () => { + beforeEach(() => { + response = { + data: {status: 'Success'}, + }; + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(200, response); + + executeQuery.poll(); + }); + + it('should update the loading icon message', (done) => { + setTimeout(() => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith( + 'pgadmin-sqleditor:loading-icon:message', + 'Loading data from the database server and rendering...' + ); + done(); + }, 0); + }); + + it('should render the results', (done) => { + setTimeout(() => { + expect(sqlEditorMock.call_render_after_poll) + .toHaveBeenCalledWith({status: 'Success'}); + done(); + }, 0); + }); + }); + + describe('when query is still running', () => { + context('when no additional information is returned', () => { + beforeEach(() => { + response = { + data: {status: 'Busy'}, + }; + + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(200, response); + executeQuery.poll(); + }); + + it('should set the flag to inform SQLEditor a query is running', (done) => { + setTimeout(() => { + expect(sqlEditorMock.setIsQueryRunning) + .toHaveBeenCalledWith(true); + done(); + }, 0); + }); + + it('should does not update history', (done) => { + setTimeout(() => { + expect(sqlEditorMock.update_msg_history).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should recursively call polling', (done) => { + setTimeout(() => { + expect(executeQuery.delayedPoll) + .toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + context('when additional information is returned', () => { + beforeEach(() => { + response = { + data: { + status: 'Busy', + result: 'Some important result', + }, + }; + + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(200, response); + executeQuery.poll(); + }); + + it('should set the flag to inform SQLEditor a query is running', (done) => { + setTimeout(() => { + expect(sqlEditorMock.setIsQueryRunning) + .toHaveBeenCalledWith(true); + done(); + }, 0); + }); + + it('should update history message', (done) => { + setTimeout(() => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith('Busy', 'Some important result', false); + done(); + }, 0); + }); + + it('should recursively call polling', (done) => { + setTimeout(() => { + expect(executeQuery.delayedPoll) + .toHaveBeenCalled(); + done(); + }, 0); + }); + }); + }); + + describe('when the application lost connection with the database', () => { + beforeEach(() => { + response = { + data: { + status: 'NotConnected', + result: 'Some interesting result', + }, + }; + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(200, response); + + executeQuery.poll(); + }); + + it('should hide the loading icon', (done) => { + setTimeout(() => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should add new entry to history and update the Messages tab and clear the result grid', (done) => { + setTimeout(() => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith( + false, + 'Some interesting result', + true + ); + done(); + }, 0); + }); + + it('should NOT enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should NOT disable the cancel button', (done) => { + setTimeout( + () => { + expect(cancelButtonSpy).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('when query was cancelled', () => { + beforeEach(() => { + response = { + data: {status: 'Cancel'}, + }; + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(200, response); + + executeQuery.poll(); + }); + + it('should hide the loading icon', (done) => { + setTimeout(() => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should add new entry to history, add cancellation message to Messages tab and clear the result grid', (done) => { + setTimeout(() => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith( + false, + 'Execution Cancelled!', + true + ); + done(); + }, 0); + }); + }); + }); + + describe('when an error occur', () => { + let errorMessageJson = { + errormsg: 'Some error in JSON', + }; + let errorMessageText = 'Some plain text error'; + let response; + + describe('when the connection to the server was lost', () => { + describe('when JSON response is available', () => { + beforeEach(() => { + response = {responseJSON: errorMessageJson}; + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(401, response); + + executeQuery.poll(); + }); + + it('should hide the loading icon', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should reset last query information', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.resetQueryHistoryObject) + .toHaveBeenCalledWith(sqlEditorMock); + done(); + }, 0); + }); + + it('should highlight the error in the SQL panel', (done) => { + setTimeout( + () => { + expect(sqlEditorMock._highlight_error) + .toHaveBeenCalledWith('Some error in JSON'); + done(); + }, 0); + }); + + it('should add new entry to history and update the Messages tab', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith(false, 'Some error in JSON'); + done(); + }, 0); + }); + + it('should enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should disable the cancel button', (done) => { + setTimeout( + () => { + expect(cancelButtonSpy).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + }); + describe('when no JSON response is available', () => { + beforeEach(() => { + response = {errormsg: errorMessageText}; + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(401, response); + + executeQuery.poll(); + }); + + it('should hide the loading icon', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should reset last query information', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.resetQueryHistoryObject) + .toHaveBeenCalledWith(sqlEditorMock); + done(); + }, 0); + }); + + it('should highlight the error in the SQL panel', (done) => { + setTimeout( + () => { + expect(sqlEditorMock._highlight_error) + .toHaveBeenCalledWith('Some plain text error'); + done(); + }, 0); + }); + + it('should add new entry to history and update the Messages tab', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith(false, 'Some plain text error'); + done(); + }, 0); + }); + + it('should not enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should not disable the cancel button', (done) => { + setTimeout( + () => { + expect(cancelButtonSpy).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('when cannot reach the Python Server', () => { + beforeEach(() => { + response = {readyState: 0}; + networkMock.onGet('/sqleditor/query_tool/poll/123').reply(401, response); + + executeQuery.poll(); + }); + + it('should hide the loading icon', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should reset last query information', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.resetQueryHistoryObject) + .toHaveBeenCalledWith(sqlEditorMock); + done(); + }, 0); + }); + + it('should not highlight the error in the SQL panel', (done) => { + setTimeout( + () => { + expect(sqlEditorMock._highlight_error).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should add new entry to history and update the Messages tab', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith(false, 'Not connected to the server or the connection to the server has been closed.'); + done(); + }, 0); + }); + + it('should enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should disable the cancel button', (done) => { + setTimeout( + () => { + expect(cancelButtonSpy).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + }); + }); + }); + }); + }); + + describe('#execute', () => { + let response; + + beforeEach(() => { + response = { + 'info': '', + 'errormsg': '', + 'data': { + 'status': true, + 'can_edit': false, + 'info_notifier_timeout': 5, + 'result': '2', + 'can_filter': false, + }, + 'result': null, + 'success': 1, + }; + }); + + context('when the SQL statement is empty', () => { + it('should return without executing', (done) => { + let wasNetworkCalled = false; + networkMock.onAny('/sqleditor/query_tool/start/123').reply(() => { + wasNetworkCalled = true; + }); + + executeQuery.execute('', {}); + + setTimeout(() => { + expect(wasNetworkCalled).toBe(false); + done(); + }, 0); + }); + }); + + context('when the SQL statement is not empty', () => { + let pollSpy; + let jqueryPropSpy; + + beforeEach(() => { + jqueryPropSpy = spyOn($.fn, 'prop'); + sqlEditorMock.gridView = {}; + sqlEditorMock.gridView.query_tool_obj = jasmine.createSpyObj( + 'QueryToolObject', + ['removeLineClass'] + ); + }); + + describe('before the backend request', () => { + beforeEach(() => { + jasmine.clock().install(); + jasmine.clock().mockDate(startTime); + jasmine.clock().tick(50); + networkMock.onAny('/sqleditor/query_tool/start/123').reply(200, response); + pollSpy = spyOn(executeQuery, 'poll'); + executeQuery.execute('some sql query', ''); + }); + + afterEach(function () { + jasmine.clock().uninstall(); + }); + + it('should update loading text to "Initializing query execution"', () => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith( + 'pgadmin-sqleditor:loading-icon:show', + 'Initializing query execution...' + ); + }); + + it('disables the run query button', () => { + const callToProp = findJQueryCallWithSelector(jqueryPropSpy, '#btn-flash'); + + expect(callToProp).jQuerytoHaveBeenCalledWith('disabled', true); + }); + + it('enable the cancel query button', () => { + const callToProp = findJQueryCallWithSelector(jqueryPropSpy, '#btn-cancel-query'); + + expect(callToProp).jQuerytoHaveBeenCalledWith('disabled', false); + }); + + it('disable the query tool buttons', () => { + expect(sqlEditorMock.disable_tool_buttons).toHaveBeenCalledWith(true); + }); + + it('initializes the polling flags', () => { + expect(sqlEditorMock._init_polling_flags).toHaveBeenCalled(); + }); + + it('save the query', () => { + expect(sqlEditorMock.query).toEqual('some sql query'); + }); + + it('reset the number of rows that were affected', () => { + expect(sqlEditorMock.rows_affected).toBe(0); + }); + + it('reset query start time', () => { + expect(sqlEditorMock.query_start_time.getTime()).toEqual(startTime.getTime() + 50); + }); + }); + + describe('when HTTP return 200', () => { + describe('when backend informs that query started successfully', () => { + beforeEach(() => { + networkMock.onAny('/sqleditor/query_tool/start/123').reply(200, response); + pollSpy = spyOn(executeQuery, 'delayedPoll'); + executeQuery.execute('some sql query', ''); + }); + + it('should changes the loading message to "Waiting for the query execution to complete"', (done) => { + setTimeout(() => { + expect(sqlEditorMock.trigger).toHaveBeenCalledWith( + 'pgadmin-sqleditor:loading-icon:message', + 'Waiting for the query execution to complete...' + ); + done(); + }, 0); + }); + + it('should update the can edit flag', (done) => { + setTimeout(() => { + expect(sqlEditorMock.can_edit).toBe(false); + done(); + }, 0); + }); + + it('should update the can filter flag', (done) => { + setTimeout(() => { + expect(sqlEditorMock.can_filter).toBe(false); + done(); + }, 0); + }); + + it('should update information notifier timeout', (done) => { + setTimeout(() => { + expect(sqlEditorMock.info_notifier_timeout).toBe(5); + done(); + }, 0); + }); + + it('should start polling', (done) => { + setTimeout(() => { + expect(pollSpy).toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('when explain plan is not empty', () => { + it('should send the explain plan informatioon through the wire', (done) => { + networkMock.onAny('/sqleditor/query_tool/start/123').reply((config) => { + setTimeout(() => { + expect(config.data).toEqual(JSON.stringify({ + sql: 'some sql query', + explain_plan: { + buffers: true, + analyze: false, + timing: true, + summary: false, + }, + })); + + done(); + }, 0); + return [200, '']; + }); + pollSpy = spyOn(executeQuery, 'delayedPoll'); + executeQuery.execute('some sql query', { + buffers: true, + analyze: false, + timing: true, + summary: false, + }); + }); + + + }); + + describe('when backend informs that there was a problem with the query', () => { + beforeEach(() => { + response.data.status = false; + response.data.result = 'something went wrong'; + networkMock.onAny('/sqleditor/query_tool/start/123').reply(200, response); + pollSpy = spyOn(executeQuery, 'poll'); + executeQuery.execute('some sql query', ''); + }); + + it('hide the loading screen', (done) => { + setTimeout(() => { + expect(sqlEditorMock.trigger).toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('disable the cancel query button', (done) => { + setTimeout(() => { + const callToProp = findJQueryCallWithSelector(jqueryPropSpy, '#btn-cancel-query'); + + expect(callToProp).jQuerytoHaveBeenCalledWith('disabled', true); + done(); + }, 0); + }); + + it('enable the query tool buttons', (done) => { + setTimeout(() => { + expect(sqlEditorMock.disable_tool_buttons).toHaveBeenCalledWith(false); + done(); + }, 0); + }); + + it('update the history tab with the result message', (done) => { + setTimeout(() => { + expect(sqlEditorMock.update_msg_history).toHaveBeenCalledWith(false, 'something went wrong'); + done(); + }, 0); + }); + + it('highlight the error in the editor', (done) => { + setTimeout(() => { + expect(sqlEditorMock._highlight_error).toHaveBeenCalledWith('something went wrong'); + done(); + }, 0); + }); + + it('should not start polling', (done) => { + setTimeout(() => { + expect(pollSpy).not.toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('when there is a marker set in the grid', () => { + let markerClearSpy; + beforeEach(() => { + sqlEditorMock.gridView.marker = jasmine.createSpyObj( + 'GridViewMarker', + ['clear'] + ); + markerClearSpy = sqlEditorMock.gridView.marker.clear; + networkMock.onAny('/sqleditor/query_tool/start/123').reply(200, response); + pollSpy = spyOn(executeQuery, 'poll'); + executeQuery.execute('some sql query', ''); + }); + + it('should call clear function on marker', (done) => { + setTimeout(() => { + expect(markerClearSpy).toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should removes the marker', (done) => { + setTimeout(() => { + expect(sqlEditorMock.gridView.marker).toEqual(null); + done(); + }, 0); + }); + + it('should remove CSS classes from the editor', (done) => { + setTimeout(() => { + expect(sqlEditorMock.gridView.query_tool_obj.removeLineClass) + .toHaveBeenCalledWith(sqlEditorMock.marked_line_no, 'wrap', 'CodeMirror-activeline-background'); + done(); + }, 0); + }); + }); + }); + + describe('when cannot reach the Python Server', () => { + beforeEach(() => { + response = { + readyState: 0, + }; + networkMock.onAny('/sqleditor/query_tool/start/123').reply(500, response); + + + executeQuery.execute('some sql query', ''); + }); + + it('should hide the loading icon', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should not highlight the error in the SQL panel', (done) => { + setTimeout( + () => { + expect(sqlEditorMock._highlight_error).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should add new entry to history and update the Messages tab', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith(false, 'Not connected to the server or the connection to the server has been closed.'); + done(); + }, 0); + }); + + it('should enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons) + .toHaveBeenCalledWith(false); + done(); + }, 0); + }); + + it('should disable the cancel button', (done) => { + setTimeout( + () => { + const callToProp = findJQueryCallWithSelector(jqueryPropSpy, '#btn-cancel-query'); + + expect(callToProp).jQuerytoHaveBeenCalledWith('disabled', true); + done(); + }, 0); + }); + }); + + describe('when error is returned by the server', () => { + describe('when login is not required', () => { + beforeEach(() => { + userManagementMock.is_pga_login_required.and.returnValue(false); + response.errormsg = 'some error message'; + networkMock.onAny('/sqleditor/query_tool/start/123').reply(500, response); + + + executeQuery.execute('some sql query', ''); + }); + + it('should hide the loading icon', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should not highlight the error in the SQL panel', (done) => { + setTimeout( + () => { + expect(sqlEditorMock._highlight_error).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should add new entry to history and update the Messages tab', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith(false, 'some error message'); + done(); + }, 0); + }); + + it('should enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons) + .toHaveBeenCalledWith(false); + done(); + }, 0); + }); + + it('should disable the cancel button', (done) => { + setTimeout( + () => { + const callToProp = findJQueryCallWithSelector(jqueryPropSpy, '#btn-cancel-query'); + + expect(callToProp).jQuerytoHaveBeenCalledWith('disabled', true); + done(); + }, 0); + }); + + it('should not save the state', () => { + setTimeout(() => { + expect(sqlEditorMock.save_state).not.toHaveBeenCalled(); + }, 0); + }); + + it('should not display pga login', () => { + setTimeout(() => { + expect(userManagementMock.pga_login).not.toHaveBeenCalled(); + }, 0); + }); + }); + describe('when login is required', () => { + beforeEach(() => { + userManagementMock.is_pga_login_required.and.returnValue(true); + response.errormsg = 'some error message'; + networkMock.onAny('/sqleditor/query_tool/start/123').reply(500, response); + + + executeQuery.execute('some sql query', ''); + }); + + it('should hide the loading icon', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should not highlight the error in the SQL panel', (done) => { + setTimeout( + () => { + expect(sqlEditorMock._highlight_error).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should add new entry to history and update the Messages tab', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith(false, 'some error message'); + done(); + }, 0); + }); + + it('should enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons) + .toHaveBeenCalledWith(false); + done(); + }, 0); + }); + + it('should disable the cancel button', (done) => { + setTimeout( + () => { + const callToProp = findJQueryCallWithSelector(jqueryPropSpy, '#btn-cancel-query'); + + expect(callToProp).jQuerytoHaveBeenCalledWith('disabled', true); + done(); + }, 0); + }); + + it('should save the state', () => { + setTimeout(() => { + expect(sqlEditorMock.save_state).toHaveBeenCalledWith( + 'execute', + [''] + ); + }, 0); + }); + + it('should display pga login', () => { + setTimeout(() => { + expect(userManagementMock.pga_login).toHaveBeenCalled(); + }, 0); + }); + }); + describe('when a new transaction is not required', () => { + beforeEach(() => { + isNewTransactionRequiredMock.and.returnValue(false); + response.errormsg = 'some error message'; + networkMock.onAny('/sqleditor/query_tool/start/123').reply(500, response); + + + executeQuery.execute('some sql query', ''); + }); + + it('should hide the loading icon', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should not highlight the error in the SQL panel', (done) => { + setTimeout( + () => { + expect(sqlEditorMock._highlight_error).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should add new entry to history and update the Messages tab', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith(false, 'some error message'); + done(); + }, 0); + }); + + it('should enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons) + .toHaveBeenCalledWith(false); + done(); + }, 0); + }); + + it('should disable the cancel button', (done) => { + setTimeout( + () => { + const callToProp = findJQueryCallWithSelector(jqueryPropSpy, '#btn-cancel-query'); + + expect(callToProp).jQuerytoHaveBeenCalledWith('disabled', true); + done(); + }, 0); + }); + + it('should not save the state', () => { + setTimeout(() => { + expect(sqlEditorMock.save_state).not.toHaveBeenCalled(); + }, 0); + }); + + it('should not display pga login', () => { + setTimeout(() => { + expect(userManagementMock.pga_login).not.toHaveBeenCalled(); + }, 0); + }); + + it('should not initialize a new transaction', () => { + setTimeout(() => { + expect(sqlEditorMock.init_transaction).not.toHaveBeenCalled(); + }, 0); + }); + }); + + describe('when a new transaction is required', () => { + beforeEach(() => { + isNewTransactionRequiredMock.and.returnValue(true); + response.errormsg = 'some error message'; + networkMock.onAny('/sqleditor/query_tool/start/123').reply(500, response); + + executeQuery.execute('some sql query', ''); + }); + + it('should hide the loading icon', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.trigger) + .toHaveBeenCalledWith('pgadmin-sqleditor:loading-icon:hide'); + done(); + }, 0); + }); + + it('should not highlight the error in the SQL panel', (done) => { + setTimeout( + () => { + expect(sqlEditorMock._highlight_error).not + .toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should add new entry to history and update the Messages tab', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.update_msg_history) + .toHaveBeenCalledWith(false, 'some error message'); + done(); + }, 0); + }); + + it('should enable the tool buttons', (done) => { + setTimeout( + () => { + expect(sqlEditorMock.disable_tool_buttons) + .toHaveBeenCalledWith(false); + done(); + }, 0); + }); + + it('should disable the cancel button', (done) => { + setTimeout( + () => { + const callToProp = findJQueryCallWithSelector(jqueryPropSpy, '#btn-cancel-query'); + + expect(callToProp).jQuerytoHaveBeenCalledWith('disabled', true); + done(); + }, 0); + }); + + it('should save the state', () => { + setTimeout(() => { + expect(sqlEditorMock.save_state).toHaveBeenCalledWith( + 'execute', + [''] + ); + }, 0); + }); + + it('should not display pga login', () => { + setTimeout(() => { + expect(userManagementMock.pga_login).not.toHaveBeenCalled(); + }, 0); + }); + + it('should initialize a new transaction', () => { + setTimeout(() => { + expect(sqlEditorMock.init_transaction).toHaveBeenCalled(); + }, 0); + }); + }); + }); + }); + }); + + let findJQueryCallWithSelector = (jquerySpy, selector) => { + let result = undefined; + + jquerySpy.calls.all().forEach((call) => { + if (call.object.selector === selector) { + result = call; + } + }); + return result; + }; +}); + +const jQuerytoHaveBeenCalledWith = function (util) { + return { + compare: function (actual) { + let result = {}; + let expectedArgs = jasmine.util.argsToArray(arguments).slice(1); + if (actual.object === undefined || actual.object.selector === undefined) { + throw new Error('Expected a JQuery object, but got ' + jasmine.pp(actual) + '.'); + } + + result.pass = util.equals(actual.args, expectedArgs, ''); + if (result.pass) { + result.message = 'larifo'; + } else { + result.message = + 'Expected jquery with selector "' + + actual.object.selector + + '" to have been called with ' + + jasmine.pp(expectedArgs) + + ' but was called with ' + + jasmine.pp(actual.args); + } + + return result; + }, + }; +}; + diff --git a/web/regression/javascript/sqleditor/is_new_transaction_required_spec.js b/web/regression/javascript/sqleditor/is_new_transaction_required_spec.js new file mode 100644 index 00000000..d323700d --- /dev/null +++ b/web/regression/javascript/sqleditor/is_new_transaction_required_spec.js @@ -0,0 +1,65 @@ +////////////////////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////////////////// + +import {is_new_transaction_required} from '../../../pgadmin/static/js/sqleditor/is_new_transaction_required'; + +describe('#is_new_transaction_required', () => { + describe('when status is not 404', () => { + it('should return false', () => { + expect(is_new_transaction_required({ + status: 300, + })).toBe(false); + }); + }); + + describe('when status is 404', () => { + describe('when responseJSON is not present', () => { + it('should return false', () => { + expect(is_new_transaction_required({ + status: 404, + })).toBeFalsy(); + }); + }); + + describe('when responseJSON is present', () => { + describe('when info is not present inside responseJSON', () => { + it('should return false', () => { + expect(is_new_transaction_required({ + status: 404, + responseJSON: {}, + })).toBeFalsy(); + }); + }); + + describe('when info is present inside responseJSON', () => { + describe('when info value is not "DATAGRID_TRANSACTION_REQUIRED"', () => { + it('should return false', () => { + expect(is_new_transaction_required({ + status: 404, + responseJSON: { + info: 'some information', + }, + })).toBe(false); + }); + }); + + describe('when info value is "DATAGRID_TRANSACTION_REQUIRED"', () => { + it('should return false', () => { + expect(is_new_transaction_required({ + status: 404, + responseJSON: { + info: 'DATAGRID_TRANSACTION_REQUIRED', + }, + })).toBe(true); + }); + }); + }); + }); + }); +}); diff --git a/web/regression/javascript/sqleditor/query_tool_actions_spec.js b/web/regression/javascript/sqleditor/query_tool_actions_spec.js index c46b4a24..f3ac7815 100644 --- a/web/regression/javascript/sqleditor/query_tool_actions_spec.js +++ b/web/regression/javascript/sqleditor/query_tool_actions_spec.js @@ -43,7 +43,6 @@ describe('queryToolActions', () => { expect(sqlEditorController.execute_data_query).toHaveBeenCalled(); }); - }); }); @@ -51,60 +50,100 @@ describe('queryToolActions', () => { describe('when verbose and costs are not selected and buffers and timing are not selected', () => { beforeEach(() => { setUpSpies('', ''); - spyOn(queryToolActions, '_verbose').and.returnValue('OFF'); - spyOn(queryToolActions, '_costsEnabled').and.returnValue('OFF'); - spyOn(queryToolActions, '_buffers').and.returnValue('OFF'); - spyOn(queryToolActions, '_timing').and.returnValue('OFF'); + spyOn(queryToolActions, '_verbose').and.returnValue(false); + spyOn(queryToolActions, '_costsEnabled').and.returnValue(false); + spyOn(queryToolActions, '_buffers').and.returnValue(false); + spyOn(queryToolActions, '_timing').and.returnValue(false); }); + it('calls the execute function', () => { queryToolActions.explainAnalyze(sqlEditorController); - let explainAnalyzeQuery = 'EXPLAIN (FORMAT JSON, ANALYZE ON, VERBOSE OFF, COSTS OFF, BUFFERS OFF, TIMING OFF) '; - expect(sqlEditorController.execute).toHaveBeenCalledWith(explainAnalyzeQuery); + + // let explainAnalyzeQuery = 'EXPLAIN (FORMAT JSON, ANALYZE ON, VERBOSE OFF, COSTS OFF, BUFFERS OFF, TIMING OFF) '; + const explainObject = { + format: 'json', + analyze: true, + verbose: false, + costs: false, + buffers: false, + timing: false, + summary: false, + }; + + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); }); }); describe('when verbose and costs and buffers and timing are all selected', () => { beforeEach(() => { setUpSpies('', ''); - spyOn(queryToolActions, '_verbose').and.returnValue('ON'); - spyOn(queryToolActions, '_costsEnabled').and.returnValue('ON'); - spyOn(queryToolActions, '_buffers').and.returnValue('ON'); - spyOn(queryToolActions, '_timing').and.returnValue('ON'); + spyOn(queryToolActions, '_verbose').and.returnValue(true); + spyOn(queryToolActions, '_costsEnabled').and.returnValue(true); + spyOn(queryToolActions, '_buffers').and.returnValue(true); + spyOn(queryToolActions, '_timing').and.returnValue(true); }); it('calls the execute function', () => { queryToolActions.explainAnalyze(sqlEditorController); - let explainAnalyzeQuery = 'EXPLAIN (FORMAT JSON, ANALYZE ON, VERBOSE ON, COSTS ON, BUFFERS ON, TIMING ON) '; - expect(sqlEditorController.execute).toHaveBeenCalledWith(explainAnalyzeQuery); + const explainObject = { + format: 'json', + analyze: true, + verbose: true, + costs: true, + buffers: true, + timing: true, + summary: false, + }; + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); }); }); describe('when verbose is selected and costs is not selected and buffer is selected and timing is not selected', () => { beforeEach(() => { setUpSpies('', ''); - spyOn(queryToolActions, '_verbose').and.returnValue('ON'); - spyOn(queryToolActions, '_costsEnabled').and.returnValue('OFF'); - spyOn(queryToolActions, '_buffers').and.returnValue('ON'); - spyOn(queryToolActions, '_timing').and.returnValue('OFF'); + spyOn(queryToolActions, '_verbose').and.returnValue(true); + spyOn(queryToolActions, '_costsEnabled').and.returnValue(false); + spyOn(queryToolActions, '_buffers').and.returnValue(true); + spyOn(queryToolActions, '_timing').and.returnValue(false); }); it('calls the execute function', () => { queryToolActions.explainAnalyze(sqlEditorController); - let explainAnalyzeQuery = 'EXPLAIN (FORMAT JSON, ANALYZE ON, VERBOSE ON, COSTS OFF, BUFFERS ON, TIMING OFF) '; - expect(sqlEditorController.execute).toHaveBeenCalledWith(explainAnalyzeQuery); + + const explainObject = { + format: 'json', + analyze: true, + verbose: true, + costs: false, + buffers: true, + timing: false, + summary: false, + }; + + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); }); }); describe('when verbose is not selected and costs is selected and buffer is not selected and timing is selected', () => { beforeEach(() => { setUpSpies('', ''); - spyOn(queryToolActions, '_verbose').and.returnValue('OFF'); - spyOn(queryToolActions, '_costsEnabled').and.returnValue('ON'); - spyOn(queryToolActions, '_buffers').and.returnValue('OFF'); - spyOn(queryToolActions, '_timing').and.returnValue('ON'); + spyOn(queryToolActions, '_verbose').and.returnValue(false); + spyOn(queryToolActions, '_costsEnabled').and.returnValue(true); + spyOn(queryToolActions, '_buffers').and.returnValue(false); + spyOn(queryToolActions, '_timing').and.returnValue(true); }); it('calls the execute function', () => { queryToolActions.explainAnalyze(sqlEditorController); - let explainAnalyzeQuery = 'EXPLAIN (FORMAT JSON, ANALYZE ON, VERBOSE OFF, COSTS ON, BUFFERS OFF, TIMING ON) '; - expect(sqlEditorController.execute).toHaveBeenCalledWith(explainAnalyzeQuery); + + const explainObject = { + format: 'json', + analyze: true, + verbose: false, + costs: true, + buffers: false, + timing: true, + summary: false, + }; + + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); }); }); }); @@ -113,39 +152,67 @@ describe('queryToolActions', () => { describe('when verbose and costs are selected', () => { beforeEach(() => { setUpSpies('', ''); - spyOn(queryToolActions, '_verbose').and.returnValue('ON'); - spyOn(queryToolActions, '_costsEnabled').and.returnValue('ON'); + spyOn(queryToolActions, '_verbose').and.returnValue(true); + spyOn(queryToolActions, '_costsEnabled').and.returnValue(true); }); + it('calls the execute function', () => { queryToolActions.explain(sqlEditorController); - let explainQuery = 'EXPLAIN (FORMAT JSON, ANALYZE OFF, VERBOSE ON, COSTS ON, BUFFERS OFF, TIMING OFF) '; - expect(sqlEditorController.execute).toHaveBeenCalledWith(explainQuery); + const explainObject = { + format: 'json', + analyze: false, + verbose: true, + costs: true, + buffers: false, + timing: false, + summary: false, + }; + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); }); }); describe('when verbose and costs are not selected', () => { beforeEach(() => { setUpSpies('', ''); - spyOn(queryToolActions, '_verbose').and.returnValue('OFF'); - spyOn(queryToolActions, '_costsEnabled').and.returnValue('OFF'); + spyOn(queryToolActions, '_verbose').and.returnValue(false); + spyOn(queryToolActions, '_costsEnabled').and.returnValue(false); }); + it('calls the execute function', () => { queryToolActions.explain(sqlEditorController); - let explainQuery = 'EXPLAIN (FORMAT JSON, ANALYZE OFF, VERBOSE OFF, COSTS OFF, BUFFERS OFF, TIMING OFF) '; - expect(sqlEditorController.execute).toHaveBeenCalledWith(explainQuery); + const explainObject = { + format: 'json', + analyze: false, + verbose: false, + costs: false, + buffers: false, + timing: false, + summary: false, + }; + + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); }); }); describe('when verbose is selected and costs is not selected', () => { beforeEach(() => { setUpSpies('', ''); - spyOn(queryToolActions, '_verbose').and.returnValue('ON'); - spyOn(queryToolActions, '_costsEnabled').and.returnValue('OFF'); + spyOn(queryToolActions, '_verbose').and.returnValue(true); + spyOn(queryToolActions, '_costsEnabled').and.returnValue(false); }); + it('calls the execute function', () => { queryToolActions.explain(sqlEditorController); - let explainQuery = 'EXPLAIN (FORMAT JSON, ANALYZE OFF, VERBOSE ON, COSTS OFF, BUFFERS OFF, TIMING OFF) '; - expect(sqlEditorController.execute).toHaveBeenCalledWith(explainQuery); + const explainObject = { + format: 'json', + analyze: false, + verbose: true, + costs: false, + buffers: false, + timing: false, + summary: false, + }; + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); }); }); });