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 authored and Dave Page committed 2019-07-03 13:57:56 +01:00
1 parent 15556f9f89
commit 0340b8fb28
25 files changed
+425 -249

No files matched your search

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 84 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 122 KiB

+4 -2
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 To generate a graphical explain diagram, open the *Explain* tab, and select
*Explain*, *Explain Analyze*, or one or more options from the *Explain options* *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 cannot be displayed graphically. Hover over an icon on the *Explain* tab to
review information about that item; a popup window will display information 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 Use the download button on top left corner of the *Explain* canvas to download
the plan as an SVG file. the plan as an SVG file.
+1
View File
@@ -11,6 +11,7 @@ notes for it.
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
release_notes_4_11
release_notes_4_10 release_notes_4_10
release_notes_4_9 release_notes_4_9
release_notes_4_8 release_notes_4_8
+18
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.
@@ -30,7 +30,6 @@
.pg-explain-stats-btn { .pg-explain-stats-btn {
top: 5px; top: 5px;
min-width: 25px; min-width: 25px;
border: 1px solid transparent;
pointer-events: none; pointer-events: none;
font-size: 0.75rem; font-size: 0.75rem;
} }
+14 -8
View File
@@ -427,14 +427,7 @@ define('pgadmin.misc.explain', [
// Calculate co-ordinates for tooltip // Calculate co-ordinates for tooltip
var toolTipX = ((currentXpos + pWIDTH) * zoomFactor - graphContainer.scrollLeft()); var toolTipX = ((currentXpos + pWIDTH) * zoomFactor - graphContainer.scrollLeft());
var toolTipY = ((currentYpos + pHEIGHT) * zoomFactor - graphContainer.scrollTop()); var toolTipY = ((currentYpos) * 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));
toolTipX = toolTipX < 0 ? 0 : (toolTipX); toolTipX = toolTipX < 0 ? 0 : (toolTipX);
toolTipY = toolTipY < 0 ? 0 : (toolTipY); toolTipY = toolTipY < 0 ? 0 : (toolTipY);
@@ -727,6 +720,19 @@ define('pgadmin.misc.explain', [
delete data ['Triggers']; 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; return data;
}, },
toJSON: function() { toJSON: function() {
@@ -15,81 +15,102 @@ let StatisticsModel = Backbone.Model.extend({
defaults: { defaults: {
JIT: [], JIT: [],
Triggers: [], Triggers: [],
Summary: {},
}, },
set_statistics: function(toolTipContainer) { set_statistics: function(toolTipContainer) {
var jit_stats = this.get('JIT'), 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 || 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').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 // Empty the tooltip content if it has any and add new data
toolTipContainer.empty();
if (Object.keys(jit_stats).length == 0 && 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; return;
} }
var tooltip = $('<table></table>', { toolTipContainer.empty();
class: 'pgadmin-tooltip-table', toolTipContainer.append(tooltip);
}).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>
`);
}
});
}
// Show toolTip at respective x,y coordinates // Show toolTip at respective x,y coordinates
toolTipContainer.css({ toolTipContainer.css({
@@ -104,12 +125,13 @@ let StatisticsModel = Backbone.Model.extend({
}); });
// Remove tooltip when mouse is out from node's area // 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.empty();
toolTipContainer.css({ toolTipContainer.css({
'opacity': '0', 'opacity': '0',
'left': 0, 'left': 0,
'top': 0, 'top': 0,
'right': '',
}); });
}); });
}, },
+2 -1
View File
@@ -282,8 +282,9 @@ function keyboardShortcutsQueryTool(
currLi = currLi.next(); currLi = currLi.next();
} }
/*do not focus on divider and disabled */ /*do not focus on divider, disabled and d-none */
while(currLi.hasClass('dropdown-divider') while(currLi.hasClass('dropdown-divider')
|| currLi.hasClass('d-none')
|| currLi.find('.dropdown-item').first().hasClass('disabled')) { || currLi.find('.dropdown-item').first().hasClass('disabled')) {
if(keyCode === UP_KEY) { if(keyCode === UP_KEY) {
currLi = currLi.prev(); currLi = currLi.prev();
@@ -26,6 +26,14 @@ let queryToolActions = {
return !$('.explain-timing').hasClass('visibility-hidden'); return !$('.explain-timing').hasClass('visibility-hidden');
}, },
_summary: function () {
return !$('.explain-summary').hasClass('visibility-hidden');
},
_settings: function () {
return !$('.explain-settings').hasClass('visibility-hidden');
},
_clearMessageTab: function () { _clearMessageTab: function () {
$('.sql-editor-message').html(''); $('.sql-editor-message').html('');
}, },
@@ -41,36 +49,31 @@ let queryToolActions = {
}, },
explainAnalyze: function (sqlEditorController) { explainAnalyze: function (sqlEditorController) {
let costEnabled = this._costsEnabled();
let verbose = this._verbose();
let buffers = this._buffers();
let timing = this._timing();
const explainObject = { const explainObject = {
format: 'json', format: 'json',
analyze: true, analyze: true,
verbose: verbose, verbose: this._verbose(),
costs: costEnabled, costs: this._costsEnabled(),
buffers: buffers, buffers: this._buffers(),
timing: timing, timing: this._timing(),
summary: false, summary: this._summary(),
settings: this._settings(),
}; };
this._clearMessageTab(); this._clearMessageTab();
sqlEditorController.execute(explainObject); sqlEditorController.execute(explainObject);
}, },
explain: function (sqlEditorController) { 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 = { const explainObject = {
format: 'json', format: 'json',
analyze: false, analyze: false,
verbose: verbose, verbose: this._verbose(),
costs: costEnabled, costs: this._costsEnabled(),
buffers: false, buffers: false,
timing: false, timing: false,
summary: false, summary: this._summary(),
settings: this._settings(),
}; };
this._clearMessageTab(); this._clearMessageTab();
sqlEditorController.execute(explainObject); sqlEditorController.execute(explainObject);
@@ -134,6 +134,20 @@ function updateUIPreferences(sqlEditor) {
$el.find('.explain-timing').addClass('visibility-hidden'); $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 */ /* Connection status check */
/* remove the status checker if present */ /* remove the status checker if present */
if(sqlEditor.connIntervalId != null) { if(sqlEditor.connIntervalId != null) {
+8 -1
View File
@@ -234,6 +234,11 @@ def panel(trans_id, is_query_tool, editor_title):
else: else:
server_type = None 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 title has slash(es) in it then replace it
if request.args and request.args['fslashes'] != '': if request.args and request.args['fslashes'] != '':
try: try:
@@ -311,6 +316,7 @@ def panel(trans_id, is_query_tool, editor_title):
is_desktop_mode=app.PGADMIN_RUNTIME, is_desktop_mode=app.PGADMIN_RUNTIME,
is_linux=is_linux_platform, is_linux=is_linux_platform,
server_type=server_type, server_type=server_type,
server_ver=server_ver,
client_platform=user_agent.platform, client_platform=user_agent.platform,
bgcolor=bgcolor, bgcolor=bgcolor,
fgcolor=fgcolor, fgcolor=fgcolor,
@@ -408,7 +414,8 @@ def initialize_query_tool(sgid, sid, did=None):
return make_json_response( return make_json_response(
data={ data={
'gridTransId': trans_id 'gridTransId': trans_id,
'serverVersion': manager.version,
} }
) )
@@ -275,6 +275,7 @@ define('pgadmin.datagrid', [
baseUrl = url_for('datagrid.panel', url_params) + baseUrl = url_for('datagrid.panel', url_params) +
'?' + 'query_url=' + encodeURI(trans_obj.sURL) + '?' + 'query_url=' + encodeURI(trans_obj.sURL) +
'&server_type=' + encodeURIComponent(trans_obj.server_type) + '&server_type=' + encodeURIComponent(trans_obj.server_type) +
'&server_ver=' + trans_obj.serverVersion+
'&fslashes=' + titileForURLObj.slashLocations; '&fslashes=' + titileForURLObj.slashLocations;
if (self.preferences.new_browser_tab) { 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 // add a load listener to the window so that the title gets changed on page load
newWin.addEventListener('load', function() { newWin.addEventListener('load', function() {
newWin.document.title = panel_title; 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 { } else {
@@ -285,6 +285,18 @@
<span> {{ _('Timing') }} </span> <span> {{ _('Timing') }} </span>
</a> </a>
</li> </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> </ul>
</div> </div>
<div class="btn-group mr-1" role="group" aria-label=""> <div class="btn-group mr-1" role="group" aria-label="">
@@ -412,7 +424,8 @@
script_type_url, script_type_url,
"{{ server_type }}", "{{ server_type }}",
{{ url_params|safe}}, {{ url_params|safe}},
'{{ layout|safe }}' '{{ layout|safe }}',
{{ server_ver }}
); );
}); });
{% endblock %} {% endblock %}
-33
View File
@@ -95,7 +95,6 @@ class SqlEditorModule(PgAdminModule):
return [ return [
'sqleditor.view_data_start', 'sqleditor.view_data_start',
'sqleditor.query_tool_start', 'sqleditor.query_tool_start',
'sqleditor.query_tool_preferences',
'sqleditor.poll', 'sqleditor.poll',
'sqleditor.fetch', 'sqleditor.fetch',
'sqleditor.fetch_all', 'sqleditor.fetch_all',
@@ -330,38 +329,6 @@ def extract_sql_from_network_parameters(request_data, request_arguments,
return request_arguments or request_form_data 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') @blueprint.route('/poll/<int:trans_id>', methods=["GET"], endpoint='poll')
@login_required @login_required
def poll(trans_id): def poll(trans_id):
@@ -81,6 +81,7 @@ define('tools.querytool', [
this.handler.preferences = this.preferences; this.handler.preferences = this.preferences;
this.connIntervalId = null; this.connIntervalId = null;
this.layout = opts.layout; this.layout = opts.layout;
this.set_server_version(opts.server_ver);
}, },
// Bind all the events // Bind all the events
@@ -121,6 +122,8 @@ define('tools.querytool', [
'click #btn-explain-costs': 'on_explain_costs', 'click #btn-explain-costs': 'on_explain_costs',
'click #btn-explain-buffers': 'on_explain_buffers', 'click #btn-explain-buffers': 'on_explain_buffers',
'click #btn-explain-timing': 'on_explain_timing', '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', 'change .limit': 'on_limit_change',
'keydown': 'keyAction', 'keydown': 'keyAction',
// Comment options // Comment options
@@ -166,6 +169,22 @@ define('tools.querytool', [
docker.addPanel('notifications', wcDocker.DOCK.STACKED, data_output_panel); 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. // This function is used to render the template.
render: function() { render: function() {
var self = this; 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) { do_not_close_menu: function(ev) {
ev.stopPropagation(); ev.stopPropagation();
}, },
@@ -2117,7 +2161,7 @@ define('tools.querytool', [
* header and loading icon and start execution of the sql query. * header and loading icon and start execution of the sql query.
*/ */
start: function(transId, is_query_tool, editor_title, script_type_url, 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; var self = this;
@@ -2140,6 +2184,7 @@ define('tools.querytool', [
el: self.container, el: self.container,
handler: self, handler: self,
layout: layout, layout: layout,
server_ver: server_ver,
}); });
self.transId = self.gridView.transId = transId; 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-costs', self._explain_costs, self);
self.on('pgadmin-sqleditor:button:explain-buffers', self._explain_buffers, 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-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 // Indentation related
self.on('pgadmin-sqleditor:indent_selected_code', self._indent_selected_code, self); self.on('pgadmin-sqleditor:indent_selected_code', self._indent_selected_code, self);
self.on('pgadmin-sqleditor:unindent_selected_code', self._unindent_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) { _toggle_explain_option: function(type) {
let self = this; let selector = `.explain-${type}`;
$.ajax({ $(selector).toggleClass('visibility-hidden');
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);
});
}, },
// This function will toggle "verbose" option in explain // This function will toggle "verbose" option in explain
_explain_verbose: function() { _explain_verbose: function() {
var self = this; this._toggle_explain_option('verbose');
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 function will toggle "costs" option in explain // This function will toggle "costs" option in explain
_explain_costs: function() { _explain_costs: function() {
var self = this; this._toggle_explain_option('costs');
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 function will toggle "buffers" option in explain // This function will toggle "buffers" option in explain
_explain_buffers: function() { _explain_buffers: function() {
var self = this; this._toggle_explain_option('buffers');
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 function will toggle "timing" option in explain // This function will toggle "timing" option in explain
_explain_timing: function() { _explain_timing: function() {
var self = this; this._toggle_explain_option('timing');
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;
}
self.explainPreferenceUpdate( _explain_summary: function() {
'timing', { this._toggle_explain_option('summary');
'explain_timing': explain_timing, },
}, '_explain_timing'
); _explain_settings: function() {
this._toggle_explain_option('settings');
}, },
/* /*
@@ -1,15 +1,16 @@
{% import 'sql/macros/utils.macros' as UTILS %}
EXPLAIN ({% if format -%} EXPLAIN ({% if format -%}
FORMAT {{ format.upper() }}, FORMAT {{ format.upper() }}
{%- endif %}{% if analyze is defined -%} {%- endif %}{% if analyze is defined -%}
ANALYZE {{ analyze }}, , ANALYZE {{ UTILS.BOOL_TEXT(analyze) }}
{%- endif %}{% if verbose is defined -%} {%- endif %}{% if verbose is defined -%}
VERBOSE {{ verbose }}, , VERBOSE {{ UTILS.BOOL_TEXT(verbose) }}
{%- endif %}{% if costs is defined -%} {%- endif %}{% if costs is defined -%}
COSTS {{ costs }}, , COSTS {{ UTILS.BOOL_TEXT(costs) }}
{%- endif %}{% if timing is defined -%} {%- endif %}{% if timing is defined -%}
TIMING {{ timing }}, , TIMING {{ UTILS.BOOL_TEXT(timing) }}
{%- endif %}{% if summary is defined -%}
SUMMARY {{ summary }},
{%- endif %}{% if buffers is defined -%} {%- endif %}{% if buffers is defined -%}
BUFFERS {{ buffers }} , BUFFERS {{ UTILS.BOOL_TEXT(buffers) }}
{%- endif %}{% if summary is defined -%}
, SUMMARY {{ UTILS.BOOL_TEXT(summary) }}
{%- endif %}) {{ sql }} {%- endif %}) {{ 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 }}
@@ -1,13 +1,14 @@
{% import 'sql/macros/utils.macros' as UTILS %}
EXPLAIN ({% if format -%} EXPLAIN ({% if format -%}
FORMAT {{ format.upper() }}, FORMAT {{ format.upper() }}
{%- endif %}{% if analyze is defined -%} {%- endif %}{% if analyze is defined -%}
ANALYZE {{ analyze }}, , ANALYZE {{ UTILS.BOOL_TEXT(analyze) }}
{%- endif %}{% if verbose is defined -%} {%- endif %}{% if verbose is defined -%}
VERBOSE {{ verbose }}, , VERBOSE {{ UTILS.BOOL_TEXT(verbose) }}
{%- endif %}{% if costs is defined -%} {%- endif %}{% if costs is defined -%}
COSTS {{ costs }}, , COSTS {{ UTILS.BOOL_TEXT(costs) }}
{%- endif %}{% if timing is defined -%} {%- endif %}{% if timing is defined -%}
TIMING {{ timing }}, , TIMING {{ UTILS.BOOL_TEXT(timing) }}
{%- endif %}{% if buffers is defined -%} {%- endif %}{% if buffers is defined -%}
BUFFERS {{ buffers }} , BUFFERS {{ UTILS.BOOL_TEXT(buffers) }}
{%- endif %}) {{ sql }} {%- endif %}) {{ sql }}
@@ -1,10 +1,14 @@
{% import 'sql/macros/utils.macros' as UTILS %}
EXPLAIN ({% if format -%} EXPLAIN ({% if format -%}
FORMAT {{ format.upper() }}, FORMAT {{ format.upper() }}
{%- endif %}{% if analyze is defined -%} {%- endif %}{% if analyze is defined -%}
ANALYZE {{ analyze }},{%- endif %}{% if verbose is defined -%} , ANALYZE {{ UTILS.BOOL_TEXT(analyze) }}
VERBOSE {{ verbose }}, {%- endif %}{% if verbose is defined -%}
, VERBOSE {{ UTILS.BOOL_TEXT(verbose) }}
{%- endif %}{% if costs is defined -%} {%- 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 -%} {%- endif %}{% if buffers is defined -%}
BUFFERS {{ buffers }} , BUFFERS {{ UTILS.BOOL_TEXT(buffers) }}
{%- endif %}) {{ sql }} {%- endif %}) {{ sql }}
@@ -10,10 +10,11 @@
import os import os
from flask import Flask, render_template from flask import Flask, render_template
from jinja2 import FileSystemLoader from jinja2 import FileSystemLoader, ChoiceLoader
from pgadmin import VersionedTemplateLoader from pgadmin import VersionedTemplateLoader
from pgadmin.utils.route import BaseTestGenerator from pgadmin.utils.route import BaseTestGenerator
from pgadmin import tools
class TestExplainPlanTemplates(BaseTestGenerator): class TestExplainPlanTemplates(BaseTestGenerator):
@@ -34,9 +35,9 @@ class TestExplainPlanTemplates(BaseTestGenerator):
), ),
sql_statement='SELECT * FROM places', sql_statement='SELECT * FROM places',
expected_return_value='EXPLAIN ' expected_return_value='EXPLAIN '
'(FORMAT XML,ANALYZE True,' '(FORMAT XML, ANALYZE true, '
'VERBOSE True,COSTS False,' 'VERBOSE true, COSTS false, '
'BUFFERS True) SELECT * FROM places' 'BUFFERS true) SELECT * FROM places'
) )
), ),
( (
@@ -52,7 +53,7 @@ class TestExplainPlanTemplates(BaseTestGenerator):
), ),
sql_statement='SELECT * FROM places', sql_statement='SELECT * FROM places',
expected_return_value='EXPLAIN ' expected_return_value='EXPLAIN '
'(FORMAT JSON,BUFFERS True) ' '(FORMAT JSON, BUFFERS true) '
'SELECT * FROM places' 'SELECT * FROM places'
) )
), ),
@@ -70,8 +71,8 @@ class TestExplainPlanTemplates(BaseTestGenerator):
), ),
sql_statement='SELECT * FROM places', sql_statement='SELECT * FROM places',
expected_return_value='EXPLAIN ' expected_return_value='EXPLAIN '
'(FORMAT JSON,TIMING False,' '(FORMAT JSON, TIMING false, '
'BUFFERS True) SELECT * FROM places' 'BUFFERS true) SELECT * FROM places'
) )
), ),
( (
@@ -89,8 +90,30 @@ class TestExplainPlanTemplates(BaseTestGenerator):
), ),
sql_statement='SELECT * FROM places', sql_statement='SELECT * FROM places',
expected_return_value='EXPLAIN ' expected_return_value='EXPLAIN '
'(FORMAT YAML,TIMING False,' '(FORMAT YAML, TIMING false, '
'SUMMARY True,BUFFERS True) ' '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' 'SELECT * FROM places'
) )
), ),
@@ -153,6 +176,11 @@ class TestExplainPlanTemplates(BaseTestGenerator):
class FakeApp(Flask): class FakeApp(Flask):
def __init__(self): def __init__(self):
super(FakeApp, self).__init__("") super(FakeApp, self).__init__("")
self.jinja_loader = FileSystemLoader( self.jinja_loader = ChoiceLoader([
os.path.dirname(os.path.realpath(__file__)) + "/../templates" FileSystemLoader(
) os.path.dirname(os.path.realpath(__file__)) + "/../templates"
),
FileSystemLoader(
os.path.join(os.path.dirname(tools.__file__), 'templates')
)
])
@@ -62,6 +62,18 @@ def RegisterQueryToolPreferences(self):
category_label=gettext('Explain') 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( self.auto_commit = self.preference.register(
'Options', 'auto_commit', 'Options', 'auto_commit',
gettext("Auto commit?"), 'boolean', True, gettext("Auto commit?"), 'boolean', True,
@@ -0,0 +1,3 @@
{% macro BOOL_TEXT(bool_val) %}
{% if bool_val %}true{% else %}false{% endif %}
{% endmacro %}
@@ -30,6 +30,7 @@ describe('ExplainStatistics', () => {
statsModel.set('JIT', []); statsModel.set('JIT', []);
statsModel.set('Triggers', []); statsModel.set('Triggers', []);
statsModel.set('Summary', {});
statsModel.set_statistics(tooltipContainer); statsModel.set_statistics(tooltipContainer);
expect($('.pg-explain-stats-area').hasClass('d-none')).toEqual(true); expect($('.pg-explain-stats-area').hasClass('d-none')).toEqual(true);
@@ -93,4 +94,37 @@ describe('ExplainStatistics', () => {
expect(tooltipContainer.css('opacity')).toEqual('0'); 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');
});
});
}); });
@@ -55,6 +55,8 @@ describe('queryToolActions', () => {
spyOn(queryToolActions, '_costsEnabled').and.returnValue(false); spyOn(queryToolActions, '_costsEnabled').and.returnValue(false);
spyOn(queryToolActions, '_buffers').and.returnValue(false); spyOn(queryToolActions, '_buffers').and.returnValue(false);
spyOn(queryToolActions, '_timing').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', () => { it('calls the execute function', () => {
@@ -69,19 +71,22 @@ describe('queryToolActions', () => {
buffers: false, buffers: false,
timing: false, timing: false,
summary: false, summary: false,
settings: false,
}; };
expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject);
}); });
}); });
describe('when verbose and costs and buffers and timing are all selected', () => { describe('when all options are selected', () => {
beforeEach(() => { beforeEach(() => {
setUpSpies('', ''); setUpSpies('', '');
spyOn(queryToolActions, '_verbose').and.returnValue(true); spyOn(queryToolActions, '_verbose').and.returnValue(true);
spyOn(queryToolActions, '_costsEnabled').and.returnValue(true); spyOn(queryToolActions, '_costsEnabled').and.returnValue(true);
spyOn(queryToolActions, '_buffers').and.returnValue(true); spyOn(queryToolActions, '_buffers').and.returnValue(true);
spyOn(queryToolActions, '_timing').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', () => { it('calls the execute function', () => {
queryToolActions.explainAnalyze(sqlEditorController); queryToolActions.explainAnalyze(sqlEditorController);
@@ -92,7 +97,8 @@ describe('queryToolActions', () => {
costs: true, costs: true,
buffers: true, buffers: true,
timing: true, timing: true,
summary: false, summary: true,
settings: true,
}; };
expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject);
}); });
@@ -105,6 +111,8 @@ describe('queryToolActions', () => {
spyOn(queryToolActions, '_costsEnabled').and.returnValue(false); spyOn(queryToolActions, '_costsEnabled').and.returnValue(false);
spyOn(queryToolActions, '_buffers').and.returnValue(true); spyOn(queryToolActions, '_buffers').and.returnValue(true);
spyOn(queryToolActions, '_timing').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', () => { it('calls the execute function', () => {
queryToolActions.explainAnalyze(sqlEditorController); queryToolActions.explainAnalyze(sqlEditorController);
@@ -117,6 +125,7 @@ describe('queryToolActions', () => {
buffers: true, buffers: true,
timing: false, timing: false,
summary: false, summary: false,
settings: false,
}; };
expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject);
@@ -130,6 +139,8 @@ describe('queryToolActions', () => {
spyOn(queryToolActions, '_costsEnabled').and.returnValue(true); spyOn(queryToolActions, '_costsEnabled').and.returnValue(true);
spyOn(queryToolActions, '_buffers').and.returnValue(false); spyOn(queryToolActions, '_buffers').and.returnValue(false);
spyOn(queryToolActions, '_timing').and.returnValue(true); spyOn(queryToolActions, '_timing').and.returnValue(true);
spyOn(queryToolActions, '_summary').and.returnValue(false);
spyOn(queryToolActions, '_settings').and.returnValue(false);
}); });
it('calls the execute function', () => { it('calls the execute function', () => {
queryToolActions.explainAnalyze(sqlEditorController); queryToolActions.explainAnalyze(sqlEditorController);
@@ -142,6 +153,35 @@ describe('queryToolActions', () => {
buffers: false, buffers: false,
timing: true, timing: true,
summary: false, 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); expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject);
@@ -155,6 +195,10 @@ describe('queryToolActions', () => {
setUpSpies('', ''); setUpSpies('', '');
spyOn(queryToolActions, '_verbose').and.returnValue(true); spyOn(queryToolActions, '_verbose').and.returnValue(true);
spyOn(queryToolActions, '_costsEnabled').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', () => { it('calls the execute function', () => {
@@ -167,6 +211,7 @@ describe('queryToolActions', () => {
buffers: false, buffers: false,
timing: false, timing: false,
summary: false, summary: false,
settings: false,
}; };
expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject);
}); });
@@ -177,6 +222,8 @@ describe('queryToolActions', () => {
setUpSpies('', ''); setUpSpies('', '');
spyOn(queryToolActions, '_verbose').and.returnValue(false); spyOn(queryToolActions, '_verbose').and.returnValue(false);
spyOn(queryToolActions, '_costsEnabled').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', () => { it('calls the execute function', () => {
@@ -189,6 +236,7 @@ describe('queryToolActions', () => {
buffers: false, buffers: false,
timing: false, timing: false,
summary: false, summary: false,
settings: false,
}; };
expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject);
@@ -200,6 +248,8 @@ describe('queryToolActions', () => {
setUpSpies('', ''); setUpSpies('', '');
spyOn(queryToolActions, '_verbose').and.returnValue(true); spyOn(queryToolActions, '_verbose').and.returnValue(true);
spyOn(queryToolActions, '_costsEnabled').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', () => { it('calls the execute function', () => {
@@ -212,6 +262,7 @@ describe('queryToolActions', () => {
buffers: false, buffers: false,
timing: false, timing: false,
summary: false, summary: false,
settings: false,
}; };
expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject); expect(sqlEditorController.execute).toHaveBeenCalledWith(explainObject);
}); });