diff --git a/docs/en_US/images/query_output_explain.png b/docs/en_US/images/query_output_explain.png old mode 100755 new mode 100644 index 152d1e404..210d5617b Binary files a/docs/en_US/images/query_output_explain.png and b/docs/en_US/images/query_output_explain.png differ diff --git a/docs/en_US/images/query_output_explain_details.png b/docs/en_US/images/query_output_explain_details.png old mode 100755 new mode 100644 index 16e59a0a3..ad5a2c480 Binary files a/docs/en_US/images/query_output_explain_details.png and b/docs/en_US/images/query_output_explain_details.png differ diff --git a/docs/en_US/query_tool.rst b/docs/en_US/query_tool.rst index 2f7000e3e..d9746ddca 100644 --- a/docs/en_US/query_tool.rst +++ b/docs/en_US/query_tool.rst @@ -133,10 +133,12 @@ Use the *Explain* tab to view a graphical representation of a query: To generate a graphical explain diagram, open the *Explain* tab, and select *Explain*, *Explain Analyze*, or one or more options from the *Explain options* -menu on the *Execute/Refresh* drop-down. Please note that *EXPLAIN VERBOSE* +drop-down. Please note that *EXPLAIN VERBOSE* cannot be displayed graphically. Hover over an icon on the *Explain* tab to review information about that item; a popup window will display information -about the selected object: +about the selected object. For information on JIT statistics, triggers and a +summary, hover over the icon on top-right corner; a similar popup window will +be displayed when appropriate. Use the download button on top left corner of the *Explain* canvas to download the plan as an SVG file. diff --git a/docs/en_US/release_notes.rst b/docs/en_US/release_notes.rst index e52164a16..15733a2ba 100644 --- a/docs/en_US/release_notes.rst +++ b/docs/en_US/release_notes.rst @@ -11,6 +11,7 @@ notes for it. .. toctree:: :maxdepth: 1 + release_notes_4_11 release_notes_4_10 release_notes_4_9 release_notes_4_8 diff --git a/docs/en_US/release_notes_4_11.rst b/docs/en_US/release_notes_4_11.rst new file mode 100644 index 000000000..c534f079e --- /dev/null +++ b/docs/en_US/release_notes_4_11.rst @@ -0,0 +1,18 @@ +************ +Version 4.10 +************ + +Release date: 2019-07-25 + +This release contains a number of bug fixes and new features since the release of pgAdmin4 4.10. + +New features +************ + +| `Feature #4335 `_ - Add EXPLAIN options for SETTINGS and SUMMARY. + +Bug fixes +********* + +| `Bug #4224 `_ - Prevent flickering of large tooltips on the Graphical EXPLAIN canvas. +| `Bug #4395 `_ - EXPLAIN options should be Query Tool instance-specific. \ No newline at end of file diff --git a/web/pgadmin/misc/static/explain/css/explain.css b/web/pgadmin/misc/static/explain/css/explain.css index 7c4ee9994..ca150316f 100644 --- a/web/pgadmin/misc/static/explain/css/explain.css +++ b/web/pgadmin/misc/static/explain/css/explain.css @@ -30,7 +30,6 @@ .pg-explain-stats-btn { top: 5px; min-width: 25px; - border: 1px solid transparent; pointer-events: none; font-size: 0.75rem; } diff --git a/web/pgadmin/misc/static/explain/js/explain.js b/web/pgadmin/misc/static/explain/js/explain.js index e13df42e3..2f7983c51 100644 --- a/web/pgadmin/misc/static/explain/js/explain.js +++ b/web/pgadmin/misc/static/explain/js/explain.js @@ -427,14 +427,7 @@ define('pgadmin.misc.explain', [ // Calculate co-ordinates for tooltip var toolTipX = ((currentXpos + pWIDTH) * zoomFactor - graphContainer.scrollLeft()); - var toolTipY = ((currentYpos + pHEIGHT) * zoomFactor - graphContainer.scrollTop()); - - // Recalculate x.y if tooltip is going out of screen - if (graphContainer.width() < (toolTipX + toolTipContainer[0].clientWidth)) - toolTipX -= (toolTipContainer[0].clientWidth + (pWIDTH * zoomFactor)); - //if(document.children[0].clientHeight < (toolTipY + toolTipContainer[0].clientHeight)) - if (graphContainer.height() < (toolTipY + toolTipContainer[0].clientHeight)) - toolTipY -= (toolTipContainer[0].clientHeight + ((pHEIGHT / 2) * zoomFactor)); + var toolTipY = ((currentYpos) * zoomFactor - graphContainer.scrollTop()); toolTipX = toolTipX < 0 ? 0 : (toolTipX); toolTipY = toolTipY < 0 ? 0 : (toolTipY); @@ -727,6 +720,19 @@ define('pgadmin.misc.explain', [ delete data ['Triggers']; } + if(data) { + let summKeys = ['Planning Time', 'Execution Time'], + summary = {}; + + summKeys.forEach((key)=>{ + if (key in data) { + summary[key] = data[key]; + } + }); + + statistics.set('Summary', summary); + } + return data; }, toJSON: function() { diff --git a/web/pgadmin/misc/static/explain/js/explain_statistics.js b/web/pgadmin/misc/static/explain/js/explain_statistics.js index 4c5e52963..2a818bc5b 100644 --- a/web/pgadmin/misc/static/explain/js/explain_statistics.js +++ b/web/pgadmin/misc/static/explain/js/explain_statistics.js @@ -15,81 +15,102 @@ let StatisticsModel = Backbone.Model.extend({ defaults: { JIT: [], Triggers: [], + Summary: {}, }, set_statistics: function(toolTipContainer) { var jit_stats = this.get('JIT'), - triggers_stats = this.get('Triggers'); + triggers_stats = this.get('Triggers'), + summary = this.get('Summary'); if (Object.keys(jit_stats).length > 0 || - Object.keys(triggers_stats).length > 0) { + Object.keys(triggers_stats).length > 0 || + Object.keys(summary).length > 0) { $('.pg-explain-stats-area').removeClass('d-none'); } - $('.pg-explain-stats-area').on('mouseover', () => { + var tooltip = $('
', { + class: 'pgadmin-tooltip-table', + }); + + if (Object.keys(jit_stats).length > 0){ + tooltip.append('JIT:'); + _.each(jit_stats, function(value, key) { + key = _.escape(key); + value = _.escape(value); + tooltip.append(` + + ${key} + ${value} + + `); + }); + } + + if (Object.keys(triggers_stats).length > 0){ + tooltip.append('Triggers:'); + _.each(triggers_stats, function(triggers, key_id) { + if (triggers instanceof Object) { + _.each(triggers, function(value, key) { + if (key === 'Trigger Name') { + key = _.escape(key); + value = _.escape(value); + tooltip.append(` + + ${key} + ${value} + + `); + } else { + key = _.escape(key); + value = _.escape(value); + tooltip.append(` + + ${key} + ${value} + + `); + } + }); + } + else { + key_id = _.escape(key_id); + triggers = _.escape(triggers); + tooltip.append(` + + ${key_id} + ${triggers} + + `); + } + }); + } + + if (Object.keys(summary).length > 0){ + tooltip.append('Summary:'); + _.each(summary, function(value, key) { + key = _.escape(key); + value = _.escape(value); + tooltip.append(` + + ${key} + ${value} + + `); + }); + } + + $('.pg-explain-stats-area').off('mouseover').on('mouseover', () => { // Empty the tooltip content if it has any and add new data - toolTipContainer.empty(); + if (Object.keys(jit_stats).length == 0 && - Object.keys(triggers_stats).length == 0) { + Object.keys(triggers_stats).length == 0 && + Object.keys(summary).length == 0) { return; } - var tooltip = $('
', { - class: 'pgadmin-tooltip-table', - }).appendTo(toolTipContainer); - - if (Object.keys(jit_stats).length > 0){ - tooltip.append('JIT:'); - _.each(jit_stats, function(value, key) { - key = _.escape(key); - value = _.escape(value); - tooltip.append(` - - ${key} - ${value} - - `); - }); - } - - if (Object.keys(triggers_stats).length > 0){ - tooltip.append('Triggers:'); - _.each(triggers_stats, function(triggers, key_id) { - if (triggers instanceof Object) { - _.each(triggers, function(value, key) { - if (key === 'Trigger Name') { - key = _.escape(key); - value = _.escape(value); - tooltip.append(` - - ${key} - ${value} - - `); - } else { - key = _.escape(key); - value = _.escape(value); - tooltip.append(` - - ${key} - ${value} - - `); - } - }); - } - else { - key_id = _.escape(key_id); - triggers = _.escape(triggers); - tooltip.append(` - - ${key_id} - ${triggers} - - `); - } - }); - } + toolTipContainer.empty(); + toolTipContainer.append(tooltip); // Show toolTip at respective x,y coordinates toolTipContainer.css({ @@ -104,12 +125,13 @@ let StatisticsModel = Backbone.Model.extend({ }); // Remove tooltip when mouse is out from node's area - $('.pg-explain-stats-area').on('mouseout', () => { + $('.pg-explain-stats-area').off('mouseout').on('mouseout', () => { toolTipContainer.empty(); toolTipContainer.css({ 'opacity': '0', 'left': 0, 'top': 0, + 'right': '', }); }); }, diff --git a/web/pgadmin/static/js/keyboard_shortcuts.js b/web/pgadmin/static/js/keyboard_shortcuts.js index c565b862c..101aff5b4 100644 --- a/web/pgadmin/static/js/keyboard_shortcuts.js +++ b/web/pgadmin/static/js/keyboard_shortcuts.js @@ -282,8 +282,9 @@ function keyboardShortcutsQueryTool( currLi = currLi.next(); } - /*do not focus on divider and disabled */ + /*do not focus on divider, disabled and d-none */ while(currLi.hasClass('dropdown-divider') + || currLi.hasClass('d-none') || currLi.find('.dropdown-item').first().hasClass('disabled')) { if(keyCode === UP_KEY) { currLi = currLi.prev(); diff --git a/web/pgadmin/static/js/sqleditor/query_tool_actions.js b/web/pgadmin/static/js/sqleditor/query_tool_actions.js index 7739e9b5a..d3f6d9e17 100644 --- a/web/pgadmin/static/js/sqleditor/query_tool_actions.js +++ b/web/pgadmin/static/js/sqleditor/query_tool_actions.js @@ -26,6 +26,14 @@ let queryToolActions = { return !$('.explain-timing').hasClass('visibility-hidden'); }, + _summary: function () { + return !$('.explain-summary').hasClass('visibility-hidden'); + }, + + _settings: function () { + return !$('.explain-settings').hasClass('visibility-hidden'); + }, + _clearMessageTab: function () { $('.sql-editor-message').html(''); }, @@ -41,36 +49,31 @@ let queryToolActions = { }, explainAnalyze: function (sqlEditorController) { - let costEnabled = this._costsEnabled(); - let verbose = this._verbose(); - let buffers = this._buffers(); - let timing = this._timing(); const explainObject = { format: 'json', analyze: true, - verbose: verbose, - costs: costEnabled, - buffers: buffers, - timing: timing, - summary: false, + verbose: this._verbose(), + costs: this._costsEnabled(), + buffers: this._buffers(), + timing: this._timing(), + summary: this._summary(), + settings: this._settings(), }; this._clearMessageTab(); 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) `; const explainObject = { format: 'json', analyze: false, - verbose: verbose, - costs: costEnabled, + verbose: this._verbose(), + costs: this._costsEnabled(), buffers: false, timing: false, - summary: false, + summary: this._summary(), + settings: this._settings(), }; this._clearMessageTab(); sqlEditorController.execute(explainObject); diff --git a/web/pgadmin/static/js/sqleditor/query_tool_preferences.js b/web/pgadmin/static/js/sqleditor/query_tool_preferences.js index c3906aaab..7fd19b40d 100644 --- a/web/pgadmin/static/js/sqleditor/query_tool_preferences.js +++ b/web/pgadmin/static/js/sqleditor/query_tool_preferences.js @@ -134,6 +134,20 @@ function updateUIPreferences(sqlEditor) { $el.find('.explain-timing').addClass('visibility-hidden'); } + if (preferences.explain_summary) { + $el.find('.explain-summary').removeClass('visibility-hidden'); + } + else { + $el.find('.explain-summary').addClass('visibility-hidden'); + } + + if (preferences.explain_settings) { + $el.find('.explain-settings').removeClass('visibility-hidden'); + } + else { + $el.find('.explain-settings').addClass('visibility-hidden'); + } + /* Connection status check */ /* remove the status checker if present */ if(sqlEditor.connIntervalId != null) { diff --git a/web/pgadmin/tools/datagrid/__init__.py b/web/pgadmin/tools/datagrid/__init__.py index fbc842fc7..e8779351d 100644 --- a/web/pgadmin/tools/datagrid/__init__.py +++ b/web/pgadmin/tools/datagrid/__init__.py @@ -234,6 +234,11 @@ def panel(trans_id, is_query_tool, editor_title): else: server_type = None + if request.args and 'server_ver' in request.args: + server_ver = request.args['server_ver'] + else: + server_ver = 0 + # If title has slash(es) in it then replace it if request.args and request.args['fslashes'] != '': try: @@ -311,6 +316,7 @@ def panel(trans_id, is_query_tool, editor_title): is_desktop_mode=app.PGADMIN_RUNTIME, is_linux=is_linux_platform, server_type=server_type, + server_ver=server_ver, client_platform=user_agent.platform, bgcolor=bgcolor, fgcolor=fgcolor, @@ -408,7 +414,8 @@ def initialize_query_tool(sgid, sid, did=None): return make_json_response( data={ - 'gridTransId': trans_id + 'gridTransId': trans_id, + 'serverVersion': manager.version, } ) diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid.js b/web/pgadmin/tools/datagrid/static/js/datagrid.js index cc0951943..a405b7de9 100644 --- a/web/pgadmin/tools/datagrid/static/js/datagrid.js +++ b/web/pgadmin/tools/datagrid/static/js/datagrid.js @@ -275,6 +275,7 @@ define('pgadmin.datagrid', [ baseUrl = url_for('datagrid.panel', url_params) + '?' + 'query_url=' + encodeURI(trans_obj.sURL) + '&server_type=' + encodeURIComponent(trans_obj.server_type) + + '&server_ver=' + trans_obj.serverVersion+ '&fslashes=' + titileForURLObj.slashLocations; if (self.preferences.new_browser_tab) { @@ -283,12 +284,6 @@ define('pgadmin.datagrid', [ // add a load listener to the window so that the title gets changed on page load newWin.addEventListener('load', function() { newWin.document.title = panel_title; - - /* Set the initial version of pref cache the new window is having - * This will be used by the poller to compare with window openers - * pref cache version - */ - //newWin.pgAdmin.Browser.preference_version(pgBrowser.preference_version()); }); } else { diff --git a/web/pgadmin/tools/datagrid/templates/datagrid/index.html b/web/pgadmin/tools/datagrid/templates/datagrid/index.html index f138b9a42..65b6c4ecd 100644 --- a/web/pgadmin/tools/datagrid/templates/datagrid/index.html +++ b/web/pgadmin/tools/datagrid/templates/datagrid/index.html @@ -285,6 +285,18 @@ {{ _('Timing') }} +
  • + + + {{ _('Summary') }} + +
  • +
  • + + + {{ _('Settings') }} + +
  • @@ -412,7 +424,8 @@ script_type_url, "{{ server_type }}", {{ url_params|safe}}, - '{{ layout|safe }}' + '{{ layout|safe }}', + {{ server_ver }} ); }); {% endblock %} diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index ac923a550..1d2796ecd 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -95,7 +95,6 @@ class SqlEditorModule(PgAdminModule): return [ 'sqleditor.view_data_start', 'sqleditor.query_tool_start', - 'sqleditor.query_tool_preferences', 'sqleditor.poll', 'sqleditor.fetch', 'sqleditor.fetch_all', @@ -330,38 +329,6 @@ def extract_sql_from_network_parameters(request_data, request_arguments, return request_arguments or request_form_data -@blueprint.route( - '/query_tool/preferences/', - methods=["PUT"], endpoint='query_tool_preferences' -) -@login_required -def preferences(trans_id): - """ - This method is used to get/put explain options from/to preferences - - Args: - trans_id: unique transaction id - """ - - data = None - if request.data: - data = json.loads(request.data, encoding='utf-8') - else: - data = request.args or request.form - for k, v in data.items(): - v = bool(v) - if k == 'explain_verbose': - blueprint.explain_verbose.set(v) - elif k == 'explain_costs': - blueprint.explain_costs.set(v) - elif k == 'explain_buffers': - blueprint.explain_buffers.set(v) - elif k == 'explain_timing': - blueprint.explain_timing.set(v) - - return success_return() - - @blueprint.route('/poll/', methods=["GET"], endpoint='poll') @login_required def poll(trans_id): diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index 8bf041f68..f7f6f8977 100644 --- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js @@ -81,6 +81,7 @@ define('tools.querytool', [ this.handler.preferences = this.preferences; this.connIntervalId = null; this.layout = opts.layout; + this.set_server_version(opts.server_ver); }, // Bind all the events @@ -121,6 +122,8 @@ define('tools.querytool', [ 'click #btn-explain-costs': 'on_explain_costs', 'click #btn-explain-buffers': 'on_explain_buffers', 'click #btn-explain-timing': 'on_explain_timing', + 'click #btn-explain-summary': 'on_explain_summary', + 'click #btn-explain-settings': 'on_explain_settings', 'change .limit': 'on_limit_change', 'keydown': 'keyAction', // Comment options @@ -166,6 +169,22 @@ define('tools.querytool', [ docker.addPanel('notifications', wcDocker.DOCK.STACKED, data_output_panel); }, + set_server_version: function(server_ver) { + let self = this; + self.server_ver = server_ver; + + this.$el.find('*[data-min-ver]').map(function() { + let minVer = 0, + ele = $(this); + minVer = parseInt(ele.attr('data-min-ver')); + if(minVer > self.server_ver) { + ele.addClass('d-none'); + } else { + ele.removeClass('d-none'); + } + }); + }, + // This function is used to render the template. render: function() { var self = this; @@ -1855,6 +1874,31 @@ define('tools.querytool', [ ); }, + on_explain_summary: function(ev) { + var self = this; + + this._stopEventPropogation(ev); + + self.handler.trigger( + 'pgadmin-sqleditor:button:explain-summary', + self, + self.handler + ); + }, + + on_explain_settings: function(ev) { + var self = this; + + this._stopEventPropogation(ev); + + self.handler.trigger( + 'pgadmin-sqleditor:button:explain-settings', + self, + self.handler + ); + }, + + do_not_close_menu: function(ev) { ev.stopPropagation(); }, @@ -2117,7 +2161,7 @@ define('tools.querytool', [ * header and loading icon and start execution of the sql query. */ start: function(transId, is_query_tool, editor_title, script_type_url, - server_type, url_params, layout + server_type, url_params, layout, server_ver ) { var self = this; @@ -2140,6 +2184,7 @@ define('tools.querytool', [ el: self.container, handler: self, layout: layout, + server_ver: server_ver, }); self.transId = self.gridView.transId = transId; @@ -2233,6 +2278,8 @@ define('tools.querytool', [ self.on('pgadmin-sqleditor:button:explain-costs', self._explain_costs, self); self.on('pgadmin-sqleditor:button:explain-buffers', self._explain_buffers, self); self.on('pgadmin-sqleditor:button:explain-timing', self._explain_timing, self); + self.on('pgadmin-sqleditor:button:explain-summary', self._explain_summary, self); + self.on('pgadmin-sqleditor:button:explain-settings', self._explain_settings, self); // Indentation related self.on('pgadmin-sqleditor:indent_selected_code', self._indent_selected_code, self); self.on('pgadmin-sqleditor:unindent_selected_code', self._unindent_selected_code, self); @@ -3888,108 +3935,37 @@ define('tools.querytool', [ }, - explainPreferenceUpdate: function(subItem, data, caller) { - let self = this; - $.ajax({ - url: url_for('sqleditor.query_tool_preferences', { - 'trans_id': self.transId, - }), - method: 'PUT', - contentType: 'application/json', - data: JSON.stringify(data), - }) - .done(function(res) { - if (res.success == undefined || !res.success) { - alertify.alert(gettext('Explain options error'), - gettext('Error occurred while setting %(subItem)s option in explain.', - {subItem : subItem}) - ); - } - else - self.call_cache_preferences(); - }) - .fail(function(e) { - let msg = httpErrorHandler.handleQueryToolAjaxError( - pgAdmin, self, e, caller, [], true - ); - alertify.alert(gettext('Explain options error'), msg); - }); + _toggle_explain_option: function(type) { + let selector = `.explain-${type}`; + $(selector).toggleClass('visibility-hidden'); }, // This function will toggle "verbose" option in explain _explain_verbose: function() { - var self = this; - let explain_verbose = false; - if ($('.explain-verbose').hasClass('visibility-hidden') === true) { - $('.explain-verbose').removeClass('visibility-hidden'); - explain_verbose = true; - } else { - $('.explain-verbose').addClass('visibility-hidden'); - explain_verbose = false; - } - - self.explainPreferenceUpdate( - 'verbose', { - 'explain_verbose': explain_verbose, - }, '_explain_verbose' - ); + this._toggle_explain_option('verbose'); }, // This function will toggle "costs" option in explain _explain_costs: function() { - var self = this; - let explain_costs = false; - if ($('.explain-costs').hasClass('visibility-hidden') === true) { - $('.explain-costs').removeClass('visibility-hidden'); - explain_costs = true; - } else { - $('.explain-costs').addClass('visibility-hidden'); - explain_costs = false; - } - - self.explainPreferenceUpdate( - 'costs', { - 'explain_costs': explain_costs, - }, '_explain_costs' - ); + this._toggle_explain_option('costs'); }, // This function will toggle "buffers" option in explain _explain_buffers: function() { - var self = this; - let explain_buffers = false; - if ($('.explain-buffers').hasClass('visibility-hidden') === true) { - $('.explain-buffers').removeClass('visibility-hidden'); - explain_buffers = true; - } else { - $('.explain-buffers').addClass('visibility-hidden'); - explain_buffers = false; - } - - self.explainPreferenceUpdate( - 'buffers', { - 'explain_buffers': explain_buffers, - }, '_explain_buffers' - ); + this._toggle_explain_option('buffers'); }, // This function will toggle "timing" option in explain _explain_timing: function() { - var self = this; - let explain_timing = false; - if ($('.explain-timing').hasClass('visibility-hidden') === true) { - $('.explain-timing').removeClass('visibility-hidden'); - explain_timing = true; - } else { - $('.explain-timing').addClass('visibility-hidden'); - explain_timing = false; - } + this._toggle_explain_option('timing'); + }, - self.explainPreferenceUpdate( - 'timing', { - 'explain_timing': explain_timing, - }, '_explain_timing' - ); + _explain_summary: function() { + this._toggle_explain_option('summary'); + }, + + _explain_settings: function() { + this._toggle_explain_option('settings'); }, /* 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 index 26743f9ec..5b30a13ee 100644 --- 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 @@ -1,15 +1,16 @@ +{% import 'sql/macros/utils.macros' as UTILS %} EXPLAIN ({% if format -%} - FORMAT {{ format.upper() }}, + FORMAT {{ format.upper() }} {%- endif %}{% if analyze is defined -%} - ANALYZE {{ analyze }}, + , ANALYZE {{ UTILS.BOOL_TEXT(analyze) }} {%- endif %}{% if verbose is defined -%} - VERBOSE {{ verbose }}, + , VERBOSE {{ UTILS.BOOL_TEXT(verbose) }} {%- endif %}{% if costs is defined -%} - COSTS {{ costs }}, + , COSTS {{ UTILS.BOOL_TEXT(costs) }} {%- endif %}{% if timing is defined -%} - TIMING {{ timing }}, -{%- endif %}{% if summary is defined -%} - SUMMARY {{ summary }}, + , TIMING {{ UTILS.BOOL_TEXT(timing) }} {%- endif %}{% if buffers is defined -%} - BUFFERS {{ buffers }} + , BUFFERS {{ UTILS.BOOL_TEXT(buffers) }} +{%- endif %}{% if summary is defined -%} + , SUMMARY {{ UTILS.BOOL_TEXT(summary) }} {%- endif %}) {{ sql }} diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/12_plus/explain_plan.sql b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/12_plus/explain_plan.sql new file mode 100644 index 000000000..88ce9c916 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/12_plus/explain_plan.sql @@ -0,0 +1,18 @@ +{% import 'sql/macros/utils.macros' as UTILS %} +EXPLAIN ({% if format -%} + FORMAT {{ format.upper() }} +{%- endif %}{% if analyze is defined -%} + , ANALYZE {{ UTILS.BOOL_TEXT(analyze) }} +{%- endif %}{% if verbose is defined -%} + , VERBOSE {{ UTILS.BOOL_TEXT(verbose) }} +{%- endif %}{% if costs is defined -%} + , COSTS {{ UTILS.BOOL_TEXT(costs) }} +{%- endif %}{% if timing is defined -%} + , TIMING {{ UTILS.BOOL_TEXT(timing) }} +{%- endif %}{% if buffers is defined -%} + , BUFFERS {{ UTILS.BOOL_TEXT(buffers) }} +{%- endif %}{% if summary is defined -%} + , SUMMARY {{ UTILS.BOOL_TEXT(summary) }} +{%- endif %}{% if settings is defined -%} + , SETTINGS {{ UTILS.BOOL_TEXT(settings) }} +{%- 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 index 6540f80aa..f1bcbebc3 100644 --- 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 @@ -1,13 +1,14 @@ +{% import 'sql/macros/utils.macros' as UTILS %} EXPLAIN ({% if format -%} - FORMAT {{ format.upper() }}, + FORMAT {{ format.upper() }} {%- endif %}{% if analyze is defined -%} - ANALYZE {{ analyze }}, + , ANALYZE {{ UTILS.BOOL_TEXT(analyze) }} {%- endif %}{% if verbose is defined -%} - VERBOSE {{ verbose }}, + , VERBOSE {{ UTILS.BOOL_TEXT(verbose) }} {%- endif %}{% if costs is defined -%} - COSTS {{ costs }}, + , COSTS {{ UTILS.BOOL_TEXT(costs) }} {%- endif %}{% if timing is defined -%} - TIMING {{ timing }}, + , TIMING {{ UTILS.BOOL_TEXT(timing) }} {%- endif %}{% if buffers is defined -%} - BUFFERS {{ buffers }} + , BUFFERS {{ UTILS.BOOL_TEXT(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 index 2945de449..f1bcbebc3 100644 --- a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/explain_plan.sql +++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/explain_plan.sql @@ -1,10 +1,14 @@ +{% import 'sql/macros/utils.macros' as UTILS %} EXPLAIN ({% if format -%} - FORMAT {{ format.upper() }}, + FORMAT {{ format.upper() }} {%- endif %}{% if analyze is defined -%} - ANALYZE {{ analyze }},{%- endif %}{% if verbose is defined -%} - VERBOSE {{ verbose }}, + , ANALYZE {{ UTILS.BOOL_TEXT(analyze) }} +{%- endif %}{% if verbose is defined -%} + , VERBOSE {{ UTILS.BOOL_TEXT(verbose) }} {%- endif %}{% if costs is defined -%} - COSTS {{ costs }}, + , COSTS {{ UTILS.BOOL_TEXT(costs) }} +{%- endif %}{% if timing is defined -%} + , TIMING {{ UTILS.BOOL_TEXT(timing) }} {%- endif %}{% if buffers is defined -%} - BUFFERS {{ buffers }} + , BUFFERS {{ UTILS.BOOL_TEXT(buffers) }} {%- endif %}) {{ sql }} diff --git a/web/pgadmin/tools/sqleditor/tests/test_explain_plan_templates.py b/web/pgadmin/tools/sqleditor/tests/test_explain_plan_templates.py index 266588457..dfa6e0b7e 100644 --- a/web/pgadmin/tools/sqleditor/tests/test_explain_plan_templates.py +++ b/web/pgadmin/tools/sqleditor/tests/test_explain_plan_templates.py @@ -10,10 +10,11 @@ import os from flask import Flask, render_template -from jinja2 import FileSystemLoader +from jinja2 import FileSystemLoader, ChoiceLoader from pgadmin import VersionedTemplateLoader from pgadmin.utils.route import BaseTestGenerator +from pgadmin import tools class TestExplainPlanTemplates(BaseTestGenerator): @@ -34,9 +35,9 @@ class TestExplainPlanTemplates(BaseTestGenerator): ), sql_statement='SELECT * FROM places', expected_return_value='EXPLAIN ' - '(FORMAT XML,ANALYZE True,' - 'VERBOSE True,COSTS False,' - 'BUFFERS True) SELECT * FROM places' + '(FORMAT XML, ANALYZE true, ' + 'VERBOSE true, COSTS false, ' + 'BUFFERS true) SELECT * FROM places' ) ), ( @@ -52,7 +53,7 @@ class TestExplainPlanTemplates(BaseTestGenerator): ), sql_statement='SELECT * FROM places', expected_return_value='EXPLAIN ' - '(FORMAT JSON,BUFFERS True) ' + '(FORMAT JSON, BUFFERS true) ' 'SELECT * FROM places' ) ), @@ -70,8 +71,8 @@ class TestExplainPlanTemplates(BaseTestGenerator): ), sql_statement='SELECT * FROM places', expected_return_value='EXPLAIN ' - '(FORMAT JSON,TIMING False,' - 'BUFFERS True) SELECT * FROM places' + '(FORMAT JSON, TIMING false, ' + 'BUFFERS true) SELECT * FROM places' ) ), ( @@ -89,8 +90,30 @@ class TestExplainPlanTemplates(BaseTestGenerator): ), sql_statement='SELECT * FROM places', expected_return_value='EXPLAIN ' - '(FORMAT YAML,TIMING False,' - 'SUMMARY True,BUFFERS True) ' + '(FORMAT YAML, TIMING false, ' + 'BUFFERS true, SUMMARY true) ' + 'SELECT * FROM places' + ) + ), + ( + 'When rendering Postgres 12 template, ' + 'when settings is present,' + 'it returns the explain plan with settings', + dict( + template_path='sqleditor/sql/12_plus/explain_plan.sql', + input_parameters=dict( + sql='SELECT * FROM places', + format='json', + buffers=False, + timing=False, + summary=False, + settings=True + ), + sql_statement='SELECT * FROM places', + expected_return_value='EXPLAIN ' + '(FORMAT JSON, TIMING false, ' + 'BUFFERS false, SUMMARY false, ' + 'SETTINGS true) ' 'SELECT * FROM places' ) ), @@ -153,6 +176,11 @@ class TestExplainPlanTemplates(BaseTestGenerator): class FakeApp(Flask): def __init__(self): super(FakeApp, self).__init__("") - self.jinja_loader = FileSystemLoader( - os.path.dirname(os.path.realpath(__file__)) + "/../templates" - ) + self.jinja_loader = ChoiceLoader([ + FileSystemLoader( + os.path.dirname(os.path.realpath(__file__)) + "/../templates" + ), + FileSystemLoader( + os.path.join(os.path.dirname(tools.__file__), 'templates') + ) + ]) diff --git a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py index ca09eaecf..64c757546 100644 --- a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py +++ b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py @@ -62,6 +62,18 @@ def RegisterQueryToolPreferences(self): category_label=gettext('Explain') ) + self.explain_summary = self.preference.register( + 'Explain', 'explain_summary', + gettext("Show summary?"), 'boolean', False, + category_label=gettext('Explain') + ) + + self.explain_settings = self.preference.register( + 'Explain', 'explain_settings', + gettext("Show settings?"), 'boolean', False, + category_label=gettext('Explain') + ) + self.auto_commit = self.preference.register( 'Options', 'auto_commit', gettext("Auto commit?"), 'boolean', True, diff --git a/web/pgadmin/tools/templates/sql/macros/utils.macros b/web/pgadmin/tools/templates/sql/macros/utils.macros new file mode 100644 index 000000000..f385116ad --- /dev/null +++ b/web/pgadmin/tools/templates/sql/macros/utils.macros @@ -0,0 +1,3 @@ +{% macro BOOL_TEXT(bool_val) %} +{% if bool_val %}true{% else %}false{% endif %} +{% endmacro %} diff --git a/web/regression/javascript/misc/explain/explain_statistics_spec.js b/web/regression/javascript/misc/explain/explain_statistics_spec.js index 04e2150e9..3124cae17 100644 --- a/web/regression/javascript/misc/explain/explain_statistics_spec.js +++ b/web/regression/javascript/misc/explain/explain_statistics_spec.js @@ -30,6 +30,7 @@ describe('ExplainStatistics', () => { statsModel.set('JIT', []); statsModel.set('Triggers', []); + statsModel.set('Summary', {}); statsModel.set_statistics(tooltipContainer); expect($('.pg-explain-stats-area').hasClass('d-none')).toEqual(true); @@ -93,4 +94,37 @@ describe('ExplainStatistics', () => { expect(tooltipContainer.css('opacity')).toEqual('0'); }); }); + + describe('Summary', () => { + beforeEach(function() { + $('body').append(statsDiv); + statsModel.set('JIT', []); + statsModel.set('Triggers', []); + statsModel.set('Summary', { + 'Planning Time': 0.12, + 'Execution Time': 2.34, + }); + statsModel.set_statistics(tooltipContainer); + }); + + it('Statistics button should be visible', () => { + expect($('.pg-explain-stats-area').hasClass('d-none')).toEqual(false); + }); + + it('Mouse over event should be trigger', () => { + // Trigger mouse over event + var hoverEvent = new $.Event('mouseover'); + $('.pg-explain-stats-area').trigger(hoverEvent); + + expect(tooltipContainer.css('opacity')).toEqual('0.8'); + }); + + it('Mouse out event should be trigger', () => { + // Trigger mouse out event + var hoverEvent = new $.Event('mouseout'); + $('.pg-explain-stats-area').trigger(hoverEvent); + + expect(tooltipContainer.css('opacity')).toEqual('0'); + }); + }); }); diff --git a/web/regression/javascript/sqleditor/query_tool_actions_spec.js b/web/regression/javascript/sqleditor/query_tool_actions_spec.js index b2b4e871a..e5e486572 100644 --- a/web/regression/javascript/sqleditor/query_tool_actions_spec.js +++ b/web/regression/javascript/sqleditor/query_tool_actions_spec.js @@ -55,6 +55,8 @@ describe('queryToolActions', () => { spyOn(queryToolActions, '_costsEnabled').and.returnValue(false); spyOn(queryToolActions, '_buffers').and.returnValue(false); spyOn(queryToolActions, '_timing').and.returnValue(false); + spyOn(queryToolActions, '_summary').and.returnValue(false); + spyOn(queryToolActions, '_settings').and.returnValue(false); }); it('calls the execute function', () => { @@ -69,19 +71,22 @@ describe('queryToolActions', () => { buffers: false, timing: false, summary: false, + settings: false, }; expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); }); }); - describe('when verbose and costs and buffers and timing are all selected', () => { + describe('when all options are selected', () => { beforeEach(() => { setUpSpies('', ''); spyOn(queryToolActions, '_verbose').and.returnValue(true); spyOn(queryToolActions, '_costsEnabled').and.returnValue(true); spyOn(queryToolActions, '_buffers').and.returnValue(true); spyOn(queryToolActions, '_timing').and.returnValue(true); + spyOn(queryToolActions, '_summary').and.returnValue(true); + spyOn(queryToolActions, '_settings').and.returnValue(true); }); it('calls the execute function', () => { queryToolActions.explainAnalyze(sqlEditorController); @@ -92,7 +97,8 @@ describe('queryToolActions', () => { costs: true, buffers: true, timing: true, - summary: false, + summary: true, + settings: true, }; expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); }); @@ -105,6 +111,8 @@ describe('queryToolActions', () => { spyOn(queryToolActions, '_costsEnabled').and.returnValue(false); spyOn(queryToolActions, '_buffers').and.returnValue(true); spyOn(queryToolActions, '_timing').and.returnValue(false); + spyOn(queryToolActions, '_summary').and.returnValue(false); + spyOn(queryToolActions, '_settings').and.returnValue(false); }); it('calls the execute function', () => { queryToolActions.explainAnalyze(sqlEditorController); @@ -117,6 +125,7 @@ describe('queryToolActions', () => { buffers: true, timing: false, summary: false, + settings: false, }; expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); @@ -130,6 +139,8 @@ describe('queryToolActions', () => { spyOn(queryToolActions, '_costsEnabled').and.returnValue(true); spyOn(queryToolActions, '_buffers').and.returnValue(false); spyOn(queryToolActions, '_timing').and.returnValue(true); + spyOn(queryToolActions, '_summary').and.returnValue(false); + spyOn(queryToolActions, '_settings').and.returnValue(false); }); it('calls the execute function', () => { queryToolActions.explainAnalyze(sqlEditorController); @@ -142,6 +153,35 @@ describe('queryToolActions', () => { buffers: false, timing: true, summary: false, + settings: false, + }; + + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); + }); + }); + + describe('when all are not selected except summary and settings', () => { + beforeEach(() => { + setUpSpies('', ''); + spyOn(queryToolActions, '_verbose').and.returnValue(false); + spyOn(queryToolActions, '_costsEnabled').and.returnValue(false); + spyOn(queryToolActions, '_buffers').and.returnValue(false); + spyOn(queryToolActions, '_timing').and.returnValue(false); + spyOn(queryToolActions, '_summary').and.returnValue(true); + spyOn(queryToolActions, '_settings').and.returnValue(true); + }); + it('calls the execute function', () => { + queryToolActions.explainAnalyze(sqlEditorController); + + const explainObject = { + format: 'json', + analyze: true, + verbose: false, + costs: false, + buffers: false, + timing: false, + summary: true, + settings: true, }; expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); @@ -155,6 +195,10 @@ describe('queryToolActions', () => { setUpSpies('', ''); spyOn(queryToolActions, '_verbose').and.returnValue(true); spyOn(queryToolActions, '_costsEnabled').and.returnValue(true); + spyOn(queryToolActions, '_summary').and.returnValue(false); + spyOn(queryToolActions, '_settings').and.returnValue(false); + spyOn(queryToolActions, '_summary').and.returnValue(false); + spyOn(queryToolActions, '_settings').and.returnValue(false); }); it('calls the execute function', () => { @@ -167,6 +211,7 @@ describe('queryToolActions', () => { buffers: false, timing: false, summary: false, + settings: false, }; expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); }); @@ -177,6 +222,8 @@ describe('queryToolActions', () => { setUpSpies('', ''); spyOn(queryToolActions, '_verbose').and.returnValue(false); spyOn(queryToolActions, '_costsEnabled').and.returnValue(false); + spyOn(queryToolActions, '_summary').and.returnValue(false); + spyOn(queryToolActions, '_settings').and.returnValue(false); }); it('calls the execute function', () => { @@ -189,6 +236,7 @@ describe('queryToolActions', () => { buffers: false, timing: false, summary: false, + settings: false, }; expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); @@ -200,6 +248,8 @@ describe('queryToolActions', () => { setUpSpies('', ''); spyOn(queryToolActions, '_verbose').and.returnValue(true); spyOn(queryToolActions, '_costsEnabled').and.returnValue(false); + spyOn(queryToolActions, '_summary').and.returnValue(false); + spyOn(queryToolActions, '_settings').and.returnValue(false); }); it('calls the execute function', () => { @@ -212,6 +262,7 @@ describe('queryToolActions', () => { buffers: false, timing: false, summary: false, + settings: false, }; expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); });