Add EXPLAIN options for SETTINGS and SUMMARY. Fixes #4335

Prevent flickering of large tooltips on the Graphical EXPLAIN canvas. Fixes #4224
EXPLAIN options should be Query Tool instance-specific. Fixes #4395
This commit is contained in:
Aditya Toshniwal 2019-07-03 13:57:56 +01:00 committed by Dave Page
parent 15556f9f89
commit 0340b8fb28
25 changed files with 425 additions and 249 deletions

BIN
docs/en_US/images/query_output_explain.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 84 KiB

BIN
docs/en_US/images/query_output_explain_details.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View File

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

View File

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

View File

@ -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 <https://redmine.postgresql.org/issues/4335>`_ - Add EXPLAIN options for SETTINGS and SUMMARY.
Bug fixes
*********
| `Bug #4224 <https://redmine.postgresql.org/issues/4224>`_ - Prevent flickering of large tooltips on the Graphical EXPLAIN canvas.
| `Bug #4395 <https://redmine.postgresql.org/issues/4395>`_ - EXPLAIN options should be Query Tool instance-specific.

View File

@ -30,7 +30,6 @@
.pg-explain-stats-btn {
top: 5px;
min-width: 25px;
border: 1px solid transparent;
pointer-events: none;
font-size: 0.75rem;
}

View File

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

View File

@ -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 = $('<table></table>', {
class: 'pgadmin-tooltip-table',
});
if (Object.keys(jit_stats).length > 0){
tooltip.append('<tr><td class="label explain-tooltip">JIT:</td></tr>');
_.each(jit_stats, function(value, key) {
key = _.escape(key);
value = _.escape(value);
tooltip.append(`
<tr>
<td class="label explain-tooltip"> ${key}</td>
<td class="label explain-tooltip-val">${value}</td>
</tr>
`);
});
}
if (Object.keys(triggers_stats).length > 0){
tooltip.append('<tr><td class="label explain-tooltip">Triggers:</td></tr>');
_.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(`
<tr>
<td class="label explain-tooltip"> ${key}</td>
<td class="label explain-tooltip-val">${value}</td>
</tr>
`);
} else {
key = _.escape(key);
value = _.escape(value);
tooltip.append(`
<tr>
<td class="label explain-tooltip"> ${key}</td>
<td class="label explain-tooltip-val">${value}</td>
</tr>
`);
}
});
}
else {
key_id = _.escape(key_id);
triggers = _.escape(triggers);
tooltip.append(`
<tr>
<td class="label explain-tooltip"> ${key_id}</td>
<td class="label explain-tooltip-val">${triggers}</td>
</tr>
`);
}
});
}
if (Object.keys(summary).length > 0){
tooltip.append('<tr><td class="label explain-tooltip">Summary:</td></tr>');
_.each(summary, function(value, key) {
key = _.escape(key);
value = _.escape(value);
tooltip.append(`
<tr>
<td class="label explain-tooltip"> ${key}</td>
<td class="label explain-tooltip-val">${value}</td>
</tr>
`);
});
}
$('.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 = $('<table></table>', {
class: 'pgadmin-tooltip-table',
}).appendTo(toolTipContainer);
if (Object.keys(jit_stats).length > 0){
tooltip.append('<tr><td class="label explain-tooltip">JIT:</td></tr>');
_.each(jit_stats, function(value, key) {
key = _.escape(key);
value = _.escape(value);
tooltip.append(`
<tr>
<td class="label explain-tooltip"> ${key}</td>
<td class="label explain-tooltip-val">${value}</td>
</tr>
`);
});
}
if (Object.keys(triggers_stats).length > 0){
tooltip.append('<tr><td class="label explain-tooltip">Triggers:</td></tr>');
_.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(`
<tr>
<td class="label explain-tooltip"> ${key}</td>
<td class="label explain-tooltip-val">${value}</td>
</tr>
`);
} else {
key = _.escape(key);
value = _.escape(value);
tooltip.append(`
<tr>
<td class="label explain-tooltip"> ${key}</td>
<td class="label explain-tooltip-val">${value}</td>
</tr>
`);
}
});
}
else {
key_id = _.escape(key_id);
triggers = _.escape(triggers);
tooltip.append(`
<tr>
<td class="label explain-tooltip"> ${key_id}</td>
<td class="label explain-tooltip-val">${triggers}</td>
</tr>
`);
}
});
}
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': '',
});
});
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -285,6 +285,18 @@
<span> {{ _('Timing') }} </span>
</a>
</li>
<li data-min-ver="100000">
<a class="dropdown-item" id="btn-explain-summary" href="#" tabindex="0">
<i class="explain-summary fa fa-check visibility-hidden" aria-hidden="true"></i>
<span> {{ _('Summary') }} </span>
</a>
</li>
<li data-min-ver="120000">
<a class="dropdown-item" id="btn-explain-settings" href="#" tabindex="0">
<i class="explain-settings fa fa-check visibility-hidden" aria-hidden="true"></i>
<span> {{ _('Settings') }} </span>
</a>
</li>
</ul>
</div>
<div class="btn-group mr-1" role="group" aria-label="">
@ -412,7 +424,8 @@
script_type_url,
"{{ server_type }}",
{{ url_params|safe}},
'{{ layout|safe }}'
'{{ layout|safe }}',
{{ server_ver }}
);
});
{% endblock %}

View File

@ -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/<int:trans_id>',
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/<int:trans_id>', methods=["GET"], endpoint='poll')
@login_required
def poll(trans_id):

View File

@ -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');
},
/*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
{% macro BOOL_TEXT(bool_val) %}
{% if bool_val %}true{% else %}false{% endif %}
{% endmacro %}

View File

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

View File

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