From c6acbcb5ad3af46a0cce1cb3a5ec1b75901a4b35 Mon Sep 17 00:00:00 2001 From: Akshay Joshi Date: Thu, 14 Apr 2016 15:04:03 +0100 Subject: [PATCH] Query tool/data editor initial version --- web/pgadmin/static/css/overrides.css | 9 + web/pgadmin/tools/datagrid/__init__.py | 266 ++ .../datagrid/templates/datagrid/filter.html | 9 + .../datagrid/templates/datagrid/index.html | 53 + .../templates/datagrid/js/datagrid.js | 428 ++++ web/pgadmin/tools/sqleditor/__init__.py | 951 ++++++++ web/pgadmin/tools/sqleditor/command.py | 638 +++++ .../tools/sqleditor/static/css/sqleditor.css | 248 ++ .../templates/sqleditor/js/sqleditor.js | 2164 +++++++++++++++++ .../sqleditor/sql/9.1_plus/create.sql | 8 + .../sqleditor/sql/9.1_plus/delete.sql | 4 + .../sqleditor/sql/9.1_plus/objectname.sql | 7 + .../sqleditor/sql/9.1_plus/objectquery.sql | 12 + .../sqleditor/sql/9.1_plus/primary_keys.sql | 8 + .../sqleditor/sql/9.1_plus/update.sql | 7 + .../sqleditor/sql/9.1_plus/validate.sql | 4 + 16 files changed, 4816 insertions(+) create mode 100644 web/pgadmin/tools/datagrid/__init__.py create mode 100644 web/pgadmin/tools/datagrid/templates/datagrid/filter.html create mode 100644 web/pgadmin/tools/datagrid/templates/datagrid/index.html create mode 100644 web/pgadmin/tools/datagrid/templates/datagrid/js/datagrid.js create mode 100644 web/pgadmin/tools/sqleditor/__init__.py create mode 100644 web/pgadmin/tools/sqleditor/command.py create mode 100644 web/pgadmin/tools/sqleditor/static/css/sqleditor.css create mode 100644 web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js create mode 100644 web/pgadmin/tools/sqleditor/templates/sqleditor/sql/9.1_plus/create.sql create mode 100644 web/pgadmin/tools/sqleditor/templates/sqleditor/sql/9.1_plus/delete.sql create mode 100644 web/pgadmin/tools/sqleditor/templates/sqleditor/sql/9.1_plus/objectname.sql create mode 100644 web/pgadmin/tools/sqleditor/templates/sqleditor/sql/9.1_plus/objectquery.sql create mode 100644 web/pgadmin/tools/sqleditor/templates/sqleditor/sql/9.1_plus/primary_keys.sql create mode 100644 web/pgadmin/tools/sqleditor/templates/sqleditor/sql/9.1_plus/update.sql create mode 100644 web/pgadmin/tools/sqleditor/templates/sqleditor/sql/9.1_plus/validate.sql diff --git a/web/pgadmin/static/css/overrides.css b/web/pgadmin/static/css/overrides.css index 3843734f1..a6f3994aa 100755 --- a/web/pgadmin/static/css/overrides.css +++ b/web/pgadmin/static/css/overrides.css @@ -528,6 +528,10 @@ fieldset[disabled] .form-control { background-color: #AAA; } +.backgrid th.renderable, .backgrid td.renderable { + white-space: pre-wrap; +} + .subnode-header { background-color: #2C76B4; height: 35px; @@ -952,3 +956,8 @@ ul.nav.nav-tabs { .pgadmin-controls.SQL>.CodeMirror { height: 500px!important; } + +.wcPanelTab > div .wcTabIcon.fa { + padding-left: 0px !important; + color: black; +} diff --git a/web/pgadmin/tools/datagrid/__init__.py b/web/pgadmin/tools/datagrid/__init__.py new file mode 100644 index 000000000..4e1c7ff12 --- /dev/null +++ b/web/pgadmin/tools/datagrid/__init__.py @@ -0,0 +1,266 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2016, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""A blueprint module implementing the datagrid frame.""" +MODULE_NAME = 'datagrid' + +import pickle +import random +import json +from flask import Response, url_for, session, request, make_response +from flask.ext.babel import gettext +from flask.ext.security import login_required +from pgadmin.tools.sqleditor.command import * +from pgadmin.utils import PgAdminModule +from pgadmin.utils.ajax import make_json_response, bad_request, internal_server_error + + +class DataGridModule(PgAdminModule): + """ + class DataGridModule(PgAdminModule) + + A module class for Edit Grid derived from PgAdminModule. + """ + + LABEL = "Data Grid" + + def get_own_menuitems(self): + return {} + + def get_own_javascripts(self): + return [{ + 'name': 'pgadmin.datagrid', + 'path': url_for('datagrid.index') + "datagrid", + 'when': None + }] + + def get_panels(self): + return [] + + +blueprint = DataGridModule(MODULE_NAME, __name__, static_url_path='/static') + + +@blueprint.route("/") +@login_required +def index(): + return bad_request(errormsg=gettext('User can not call this URL directly')) + + +@blueprint.route("/css/datagrid.css") +def datagrid_css(): + return make_response( + render_template('datagrid/css/datagrid.css'), + 200, {'Content-Type': 'text/css'} + ) + + +@blueprint.route("/filter") +@login_required +def show_filter(): + return render_template(MODULE_NAME + '/filter.html') + + +@blueprint.route( + '/initialize/datagrid/////', + methods=["PUT", "POST"] + ) +@login_required +def initialize_datagrid(cmd_type, obj_type, sid, did, obj_id): + """ + This method is responsible for creating an asynchronous connection. + After creating the connection it will instantiate and initialize + the object as per the object type. It will also create a unique + transaction id and store the information into session variable. + + Args: + cmd_type: Contains value for which menu item is clicked. + obj_type: Contains type of selected object for which data grid to be render + sid: Server Id + did: Database Id + obj_id: Id of currently selected object + """ + + if request.data: + filter_sql = json.loads(request.data.decode()) + else: + filter_sql = request.args or request.form + + # Create asynchronous connection using random connection id. + conn_id = str(random.randint(1, 9999999)) + try: + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) + conn = manager.connection(did=did, conn_id=conn_id) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + # Connect the Server + status, msg = conn.connect() + if not status: + return internal_server_error(errormsg=str(msg)) + + try: + # Get the object as per the object type + command_obj = ObjectRegistry.get_object(obj_type, conn_id=conn_id, sid=sid, + did=did, obj_id=obj_id, cmd_type=cmd_type, + sql_filter=filter_sql) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + # Create a unique id for the transaction + trans_id = str(random.randint(1, 9999999)) + + if 'gridData' not in session: + sql_grid_data = dict() + else: + sql_grid_data = session['gridData'] + + # Use pickle to store the command object which will be used + # later by the sql grid module. + sql_grid_data[trans_id] = { + 'command_obj': pickle.dumps(command_obj, -1) # -1 specify the highest protocol version available + } + + # Store the grid dictionary into the session variable + session['gridData'] = sql_grid_data + + return make_json_response(data={'gridTransId': trans_id}) + + +@blueprint.route('/panel///', methods=["GET"]) +def panel(trans_id, is_query_tool, editor_title): + """ + This method calls index.html to render the data grid. + + Args: + trans_id: unique transaction id + is_query_tool: True if panel calls when query tool menu is clicked. + editor_title: Title of the editor + """ + + return render_template("datagrid/index.html", _=gettext, uniqueId=trans_id, + is_query_tool=is_query_tool, editor_title=editor_title) + + +@blueprint.route( + '/initialize/query_tool//', + methods=["PUT", "POST"] + ) +@login_required +def initialize_query_tool(sid, did): + """ + This method is responsible for instantiating and initializing + the query tool object. It will also create a unique + transaction id and store the information into session variable. + + Args: + sid: Server Id + did: Database Id + """ + + try: + command_obj = ObjectRegistry.get_object('query_tool', sid=sid, did=did) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + # Create a unique id for the transaction + trans_id = str(random.randint(1, 9999999)) + + if 'gridData' not in session: + sql_grid_data = dict() + else: + sql_grid_data = session['gridData'] + + # Use pickle to store the command object which will be used + # later by the sql grid module. + sql_grid_data[trans_id] = { + 'command_obj': pickle.dumps(command_obj, -1) # -1 specify the highest protocol version available + } + + # Store the grid dictionary into the session variable + session['gridData'] = sql_grid_data + + return make_json_response(data={'gridTransId': trans_id}) + + +@blueprint.route('/close/', methods=["GET"]) +def close(trans_id): + """ + This method is used to close the asynchronous connection + and remove the information of unique transaction id from + the session variable. + + Args: + trans_id: unique transaction id + """ + + grid_data = session['gridData'] + # Return from the function if transaction id not found + if str(trans_id) not in grid_data: + return make_json_response(data={'status': True}) + + cmd_obj_str = grid_data[str(trans_id)]['command_obj'] + # Use pickle.loads function to get the command object + cmd_obj = pickle.loads(cmd_obj_str) + + # if connection id is None then no need to release the connection + if cmd_obj.conn_id is not None: + try: + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(cmd_obj.sid) + conn = manager.connection(did=cmd_obj.did, conn_id=cmd_obj.conn_id) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + # Release the connection + if conn.connected(): + manager.release(did=cmd_obj.did, conn_id=cmd_obj.conn_id) + + # Remove the information of unique transaction id from the session variable. + grid_data.pop(str(trans_id), None) + session['gridData'] = grid_data + + return make_json_response(data={'status': True}) + + +@blueprint.route('/filter/validate///', + methods=["PUT", "POST"]) +@login_required +def validate_filter(sid, did, obj_id): + """ + This method is used to validate the sql filter. + + Args: + sid: Server Id + did: Database Id + obj_id: Id of currently selected object + """ + if request.data: + filter_sql = json.loads(request.data.decode()) + else: + filter_sql = request.args or request.form + + try: + # Create object of SQLFilter class + sql_filter_obj = SQLFilter(sid=sid, did=did, obj_id=obj_id) + + # Call validate_filter method to validate the SQL. + status, res = sql_filter_obj.validate_filter(filter_sql) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + return make_json_response(data={'status': status, 'result': res}) + + +@blueprint.route("/datagrid.js") +@login_required +def script(): + """render the required javascript""" + return Response(response=render_template("datagrid/js/datagrid.js", _=gettext), + status=200, + mimetype="application/javascript") diff --git a/web/pgadmin/tools/datagrid/templates/datagrid/filter.html b/web/pgadmin/tools/datagrid/templates/datagrid/filter.html new file mode 100644 index 000000000..34548caaa --- /dev/null +++ b/web/pgadmin/tools/datagrid/templates/datagrid/filter.html @@ -0,0 +1,9 @@ +
+ + +
\ No newline at end of file diff --git a/web/pgadmin/tools/datagrid/templates/datagrid/index.html b/web/pgadmin/tools/datagrid/templates/datagrid/index.html new file mode 100644 index 000000000..31ade7dfa --- /dev/null +++ b/web/pgadmin/tools/datagrid/templates/datagrid/index.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} +{% block title %}{{ config.APP_NAME }} - Datagrid{% endblock %} +{% block css_link %} + +{% endblock %} +{% block body %} + +
+
', + + +
+
+
+{% endblock %} + +{% block init_script %} +try { + require( + ['jquery', 'pgadmin', 'pgadmin.sqleditor'], + function($, pgAdmin) { + + var editorPanel = $('.sql-editor'), + loadingDiv = $('#fetching_data'), + msgDiv = loadingDiv.find('.sql-editor-busy-text'); + + // Get the controller object from pgAdmin.SqlEditor + var sqlEditorController = pgAdmin.SqlEditor.create(editorPanel); + + // Listen on events to show/hide loading-icon and change messages. + sqlEditorController.on('pgadmin-sqleditor:loading-icon:message', function(msg) { + msgDiv.text(msg); + }).on('pgadmin-sqleditor:loading-icon:show', function(msg) { + loadingDiv.removeClass('hide'); + msgDiv.text(msg); + }).on('pgadmin-sqleditor:loading-icon:hide', function() { + if (!loadingDiv.hasClass('hide')) { + loadingDiv.addClass('hide'); + } + }); + + // Start the query tool. + sqlEditorController.start({{ is_query_tool }}, "{{ editor_title }}"); + }); +} catch (err) { + /* Show proper error dialog */ + console.log(err); +} +{% endblock %} diff --git a/web/pgadmin/tools/datagrid/templates/datagrid/js/datagrid.js b/web/pgadmin/tools/datagrid/templates/datagrid/js/datagrid.js new file mode 100644 index 000000000..dda607d0a --- /dev/null +++ b/web/pgadmin/tools/datagrid/templates/datagrid/js/datagrid.js @@ -0,0 +1,428 @@ +define( + ['jquery','alertify', 'pgadmin', 'pgadmin.browser', 'wcdocker'], + function($, alertify, pgAdmin) { + // Some scripts do export their object in the window only. + // Generally the one, which do no have AMD support. + var wcDocker = window.wcDocker, + pgBrowser = pgAdmin.Browser; + + /* Return back, this has been called more than once */ + if (pgAdmin.DataGrid) + return pgAdmin.DataGrid; + + pgAdmin.DataGrid = { + init: function() { + if (this.initialized) + return; + this.initialized = true; + this.title_index = 1; + + // Define list of nodes on which view data option appears + var supported_nodes = [ + 'table', 'view', + 'foreign-table', 'catalog_object' + ], + + /* Enable/disable View data menu in tools based + * on node selected. if selected node is present + * in supported_nodes, menu will be enabled + * otherwise disabled. + */ + view_menu_enabled = function(obj) { + if(!_.isUndefined(obj) && !_.isNull(obj)) + return (_.indexOf(supported_nodes, obj._type) !== -1 ? true: false); + else + return false; + }; + + // Define list of nodes on which Query tool option doesn't appears + var unsupported_nodes = [ + 'server-group', 'server', 'coll-tablespace', 'tablespace', + 'coll-role', 'role', 'coll-resource_group', 'resource_group', + 'coll-database' + ], + + /* Enable/disable Query tool menu in tools based + * on node selected. if selected node is present + * in unsupported_nodes, menu will be disabled + * otherwise enabled. + */ + query_tool_menu_enabled = function(obj) { + if(!_.isUndefined(obj) && !_.isNull(obj)) + return (_.indexOf(unsupported_nodes, obj._type) !== -1 ? false: true); + else + return false; + }; + + // Define the nodes on which the menus to be appear + var menus = [{ + name: 'query_tool', module: this, applies: ['tools'], + callback: 'show_query_tool', enable: query_tool_menu_enabled, + priority: 1, label: '{{ _('Query tool') }}', + icon: 'fa fa-bolt' + }]; + + // Create context menu + for (var idx = 0; idx < supported_nodes.length; idx++) { + menus.push({ + name: 'view_first_100_rows_context_' + supported_nodes[idx], + node: supported_nodes[idx], module: this, data: {mnuid: 1}, + applies: ['context', 'object'], callback: 'show_data_grid', enable: view_menu_enabled, + category: 'view_data', priority: 101, label: '{{ _('View First 100 Rows') }}' + },{ + name: 'view_last_100_rows_context_' + supported_nodes[idx], + node: supported_nodes[idx], module: this, data: {mnuid: 2}, + applies: ['context', 'object'], callback: 'show_data_grid', enable: view_menu_enabled, + category: 'view_data', priority: 102, label: '{{ _('View Last 100 Rows') }}' + },{ + name: 'view_all_rows_context_' + supported_nodes[idx], + node: supported_nodes[idx], module: this, data: {mnuid: 3}, + applies: ['context', 'object'], callback: 'show_data_grid', enable: view_menu_enabled, + category: 'view_data', priority: 103, label: '{{ _('View All Rows') }}' + },{ + name: 'view_filtered_rows_context_' + supported_nodes[idx], + node: supported_nodes[idx], module: this, data: {mnuid: 4}, + applies: ['context', 'object'], callback: 'show_filtered_row', enable: view_menu_enabled, + category: 'view_data', priority: 104, label: '{{ _('View Filtered Rows...') }}' + }); + } + + pgAdmin.Browser.add_menu_category('view_data', '{{ _('View Data') }}', 100, 'fa fa-th'); + pgAdmin.Browser.add_menus(menus); + + // Creating a new pgAdmin.Browser frame to show the data. + var dataGridFrameType = new pgAdmin.Browser.Frame({ + name: 'frm_datagrid', + title: 'Edit Data', + showTitle: true, + isCloseable: true, + isPrivate: true, + url: 'about:blank' + }); + + // Load the newly created frame + dataGridFrameType.load(pgBrowser.docker); + }, + + // This is a callback function to show data when user click on menu item. + show_data_grid: function(data, i) { + var self = this, + d = pgAdmin.Browser.tree.itemData(i); + if (d === undefined) { + alertify.alert( + 'Data Grid Error', + 'No object selected.' + ); + return; + } + + // Get the parent data from the tree node hierarchy. + var node = pgBrowser.Nodes[d._type], + parentData = node.getTreeNodeHierarchy(i); + + // If server, database or schema is undefined then return from the function. + if (parentData.server === undefined || parentData.database === undefined) { + return; + } + // If schema, view, catalog object all are undefined then return from the function. + if (parentData.schema === undefined && parentData.view === undefined && + parentData.catalog === undefined) { + return; + } + + var nsp_name = ''; + + if (parentData.schema != undefined) { + nsp_name = parentData.schema.label; + } + else if (parentData.view != undefined) { + nsp_name = parentData.view.label; + } + else if (parentData.catalog != undefined) { + nsp_name = parentData.catalog.label; + } + + var baseUrl = "{{ url_for('datagrid.index') }}" + "initialize/datagrid/" + data.mnuid + "/" + d._type + "/" + + parentData.server._id + "/" + parentData.database._id + "/" + d._id; + + var grid_title = parentData.server.label + '-' + parentData.database.label + '-' + + nsp_name + '.' + d.label; + + // Initialize the data grid. + self.initialize_data_grid(baseUrl, grid_title, ''); + }, + + // This is a callback function to show filtered data when user click on menu item. + show_filtered_row: function(data, i) { + var self = this, + d = pgAdmin.Browser.tree.itemData(i); + if (d === undefined) { + alertify.alert( + 'Data Grid Error', + 'No object selected.' + ); + return; + } + + // Get the parent data from the tree node hierarchy. + var node = pgBrowser.Nodes[d._type], + parentData = node.getTreeNodeHierarchy(i); + + // If server or database is undefined then return from the function. + if (parentData.server === undefined || parentData.database === undefined) { + return; + } + + // If schema, view, catalog object all are undefined then return from the function. + if (parentData.schema === undefined && parentData.view === undefined && + parentData.catalog === undefined) { + return; + } + + var nsp_name = ''; + + if (parentData.schema != undefined) { + nsp_name = parentData.schema.label; + } + else if (parentData.view != undefined) { + nsp_name = parentData.view.label; + } + else if (parentData.catalog != undefined) { + nsp_name = parentData.catalog.label; + } + + // Create base url to initialize the edit grid + var baseUrl = "{{ url_for('datagrid.index') }}" + "initialize/datagrid/" + data.mnuid + "/" + d._type + "/" + + parentData.server._id + "/" + parentData.database._id + "/" + d._id; + + // Create url to validate the SQL filter + var validateUrl = "{{ url_for('datagrid.index') }}" + "filter/validate/" + + parentData.server._id + "/" + parentData.database._id + "/" + d._id; + + var grid_title = parentData.server.label + '-' + parentData.database.label + '-' + + nsp_name + '.' + d.label; + + // Create filter dialog using alertify + if (!alertify.filterDialog) { + alertify.dialog('filterDialog', function factory() { + return { + main: function(title, message) { + this.set('title', title); + this.message = message; + }, + + setup:function() { + return { + buttons:[ + { text: "OK", className: "btn btn-primary" }, + { text: "Cancel", className: "btn btn-danger" } + ], + options: { modal: 0, resizable: false, maximizable: false, pinnable: false} + }; + }, + + build:function() {}, + prepare:function() { + var $content = $(this.message), + $sql_filter = $content.find('#sql_filter'); + + this.setContent($content.get(0)); + + // Apply CodeMirror to filter text area. + this.filter_obj = CodeMirror.fromTextArea($sql_filter.get(0), { + lineNumbers: true, + lineWrapping: true, + matchBrackets: true, + indentUnit: 4, + mode: "text/x-sql" + }); + }, + + callback: function(closeEvent) { + + if (closeEvent.button.text == "{{ _('OK') }}") { + var sql = this.filter_obj.getValue(); + + // Make ajax call to include the filter by selection + $.ajax({ + url: validateUrl, + method: 'POST', + async: false, + contentType: "application/json", + data: JSON.stringify(sql), + success: function(res) { + if (res.data.status) { + // Initialize the data grid. + self.initialize_data_grid(baseUrl, grid_title, sql); + } + else { + alertify.alert( + 'Validation Error', + res.data.result + ); + } + }, + error: function(e) { + alertify.alert( + 'Validation Error', + e + ); + } + }); + } + } + }; + }); + } + + var content = ''; + $.get("{{ url_for('datagrid.index') }}" + "filter", + function(data) { + alertify.filterDialog('Data Filter', data).resizeTo(600, 400); + } + ); + }, + + initialize_data_grid: function(baseUrl, grid_title, sql_filter) { + var self = this; + + /* Ajax call to initialize the edit grid, which creates + * an asynchronous connection and create appropriate query + * for the selected node. + */ + $.ajax({ + url: baseUrl, + method: 'POST', + dataType: 'json', + contentType: "application/json", + data: JSON.stringify(sql_filter), + success: function(res) { + + /* On successfully initialization find the dashboard panel, + * create new panel and add it to the dashboard panel. + */ + var panel_title = ' Query-' + self.title_index; + self.title_index += 1; + + var dashboardPanel = pgBrowser.docker.findPanels('dashboard'); + dataGridPanel = pgBrowser.docker.addPanel('frm_datagrid', wcDocker.DOCK.STACKED, dashboardPanel[0]); + dataGridPanel.title(panel_title); + dataGridPanel.icon('fa fa-bolt'); + dataGridPanel.focus(); + + // Listen on the panel closed event. + dataGridPanel.on(wcDocker.EVENT.CLOSED, function() { + $.ajax({ + url: "{{ url_for('datagrid.index') }}" + "close/" + res.data.gridTransId, + method: 'GET' + }); + }); + + // Open the panel if frame is initialized + baseUrl = "{{ url_for('datagrid.index') }}" + "panel/" + res.data.gridTransId + "/false/" + grid_title; + var openDataGridURL = function(j) { + setTimeout(function() { + var frameInitialized = j.data('frameInitialized'); + if (frameInitialized) { + var frame = j.data('embeddedFrame'); + if (frame) { + frame.openURL(baseUrl); + } + } else { + openDataGridURL(j); + } + }, 100); + }; + openDataGridURL($(dataGridPanel)); + }, + error: function(e) { + alertify.alert( + 'SQL Tool Initialize Error', + e.responseJSON.errormsg + ); + } + }); + }, + + // This is a callback function to show query tool when user click on menu item. + show_query_tool: function(data, i) { + var self = this, + d = pgAdmin.Browser.tree.itemData(i); + if (d === undefined) { + alertify.alert( + 'Query tool Error', + 'No object selected.' + ); + return; + } + + // Get the parent data from the tree node hierarchy. + var node = pgBrowser.Nodes[d._type], + parentData = node.getTreeNodeHierarchy(i); + + // If server, database is undefined then return from the function. + if (parentData.server === undefined || parentData.database === undefined) { + return; + } + + var baseUrl = "{{ url_for('datagrid.index') }}" + "initialize/query_tool/" + parentData.server._id + + "/" + parentData.database._id; + var grid_title = parentData.database.label + ' on ' + parentData.server.user.name + '@' + + parentData.server.label ; + + var panel_title = ' Query-' + self.title_index; + self.title_index += 1; + + $.ajax({ + url: baseUrl, + method: 'POST', + dataType: 'json', + contentType: "application/json", + success: function(res) { + + /* On successfully initialization find the dashboard panel, + * create new panel and add it to the dashboard panel. + */ + var dashboardPanel = pgBrowser.docker.findPanels('dashboard'); + queryToolPanel = pgBrowser.docker.addPanel('frm_datagrid', wcDocker.DOCK.STACKED, dashboardPanel[0]); + queryToolPanel.title(panel_title); + queryToolPanel.icon('fa fa-bolt'); + queryToolPanel.focus(); + + // Listen on the panel closed event. + queryToolPanel.on(wcDocker.EVENT.CLOSED, function() { + $.ajax({ + url: "{{ url_for('datagrid.index') }}" + "close/" + res.data.gridTransId, + method: 'GET' + }); + }); + + // Open the panel if frame is initialized + baseUrl = "{{ url_for('datagrid.index') }}" + "panel/" + res.data.gridTransId + "/true/" + grid_title; + var openQueryToolURL = function(j) { + setTimeout(function() { + var frameInitialized = j.data('frameInitialized'); + if (frameInitialized) { + var frame = j.data('embeddedFrame'); + if (frame) { + frame.openURL(baseUrl); + } + } else { + openQueryToolURL(j); + } + }, 100); + }; + openQueryToolURL($(queryToolPanel)); + }, + error: function(e) { + alertify.alert( + 'Query Tool Initialize Error', + e.responseJSON.errormsg + ); + } + }); + } + }; + + return pgAdmin.DataGrid; + }); diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py new file mode 100644 index 000000000..cb2280757 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -0,0 +1,951 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2016, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""A blueprint module implementing the sqleditor frame.""" +MODULE_NAME = 'sqleditor' + +import json +import pickle +import random +from flask import Response, url_for, render_template, session, request +from flask.ext.babel import gettext +from flask.ext.security import login_required +from pgadmin.utils import PgAdminModule +from pgadmin.utils.ajax import make_json_response, bad_request, success_return, internal_server_error +from pgadmin.utils.driver import get_driver +from config import PG_DEFAULT_DRIVER +from pgadmin.tools.sqleditor.command import QueryToolCommand + + +# Async Constants +ASYNC_OK = 1 +ASYNC_READ_TIMEOUT = 2 +ASYNC_WRITE_TIMEOUT = 3 +ASYNC_NOT_CONNECTED = 4 +ASYNC_EXECUTION_ABORTED = 5 + +# Transaction status constants +TX_STATUS_IDLE = 0 +TX_STATUS__ACTIVE = 1 +TX_STATUS_INTRANS = 2 +TX_STATUS_INERROR = 3 + + +class SqlEditorModule(PgAdminModule): + """ + class SqlEditorModule(PgAdminModule) + + A module class for SQL Grid derived from PgAdminModule. + """ + + LABEL = "SQL Editor" + + def get_own_menuitems(self): + return {} + + def get_own_javascripts(self): + return [{ + 'name': 'pgadmin.sqleditor', + 'path': url_for('sqleditor.index') + "sqleditor", + 'when': None + }] + + def get_panels(self): + return [] + + def register_preferences(self): + self.items_per_page = self.preference.register( + 'display', 'items_per_page', + gettext("Items per page in grid"), 'integer', 50, + category_label=gettext('Display') + ) + +blueprint = SqlEditorModule(MODULE_NAME, __name__, static_url_path='/static') + + +@blueprint.route('/') +@login_required +def index(): + return bad_request(errormsg=gettext('User can not call this url directly')) + + +def update_session_grid_transaction(trans_id, data): + grid_data = session['gridData'] + grid_data[str(trans_id)] = data + + session['gridData'] = grid_data + + +def check_transaction_status(trans_id): + """ + This function is used to check the transaction id + is available in the session object and connection + status. + + Args: + trans_id: + + Returns: status and connection object + + """ + grid_data = session['gridData'] + + # Return from the function if transaction id not found + if str(trans_id) not in grid_data: + return False, gettext('Transaction ID not found in the session.'), None, None, None + + # Fetch the object for the specified transaction id. + # Use pickle.loads function to get the command object + session_obj = grid_data[str(trans_id)] + trans_obj = pickle.loads(session_obj['command_obj']) + + try: + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(trans_obj.sid) + conn = manager.connection(did=trans_obj.did, conn_id=trans_obj.conn_id) + except Exception as e: + return False, internal_server_error(errormsg=str(e)), None, None, None + + if conn.connected(): + return True, None, conn, trans_obj, session_obj + else: + return False, gettext('Not connected to server or connection with the server has been closed.'), \ + None, trans_obj, session_obj + + +@blueprint.route('/view_data/start/', methods=["GET"]) +@login_required +def start_view_data(trans_id): + """ + This method is used to execute query using asynchronous connection. + + Args: + trans_id: unique transaction id + """ + limit = -1 + + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id) + if status and conn is not None \ + and trans_obj is not None and session_obj is not None: + try: + + # Fetch the sql and primary_keys from the object + sql = trans_obj.get_sql() + pk_names, primary_keys = trans_obj.get_primary_keys() + + # Fetch the applied filter. + filter_applied = trans_obj.is_filter_applied() + + # Fetch the limit for the SQL query + limit = trans_obj.get_limit() + + can_edit = trans_obj.can_edit() + can_filter = trans_obj.can_filter() + + # Store the primary keys to the session object + session_obj['primary_keys'] = primary_keys + update_session_grid_transaction(trans_id, session_obj) + + # Execute sql asynchronously + status, result = conn.execute_async(sql) + except Exception as e: + return internal_server_error(errormsg=str(e)) + else: + status = False + result = error_msg + filter_applied = False + can_edit = False + can_filter = False + sql = None + + return make_json_response( + data={ + 'status': status, 'result': result, + 'filter_applied': filter_applied, + 'limit': limit, 'can_edit': can_edit, + 'can_filter': can_filter, 'sql': sql, + 'items_per_page': blueprint.items_per_page.get() + } + ) + + +@blueprint.route('/query_tool/start/', methods=["PUT", "POST"]) +@login_required +def start_query_tool(trans_id): + """ + This method is used to execute query using asynchronous connection. + + Args: + trans_id: unique transaction id + """ + + if request.data: + sql = json.loads(request.data.decode()) + else: + sql = request.args or request.form + + grid_data = session['gridData'] + + # Return from the function if transaction id not found + if str(trans_id) not in grid_data: + return make_json_response( + data={ + 'status': False, 'result': gettext('Transaction ID not found in the session.'), + 'can_edit': False, 'can_filter': False + } + ) + + # Fetch the object for the specified transaction id. + # Use pickle.loads function to get the command object + session_obj = grid_data[str(trans_id)] + trans_obj = pickle.loads(session_obj['command_obj']) + + can_edit = False + can_filter = False + + if trans_obj is not None and session_obj is not None: + conn_id = trans_obj.conn_id + + # if conn_id is None then we will have to create a new connection + if conn_id is None: + # Create asynchronous connection using random connection id. + conn_id = str(random.randint(1, 9999999)) + + try: + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(trans_obj.sid) + conn = manager.connection(did=trans_obj.did, conn_id=conn_id) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + # Connect to the Server if not connected. + if not conn.connected(): + status, msg = conn.connect() + if not status: + return internal_server_error(errormsg=str(msg)) + + if conn.connected(): + # on successful connection set the connection id to the + # transaction object + trans_obj.set_connection_id(conn_id) + + # As we changed the transaction object we need to + # restore it and update the session variable. + session_obj['command_obj'] = pickle.dumps(trans_obj, -1) + update_session_grid_transaction(trans_id, session_obj) + + # If auto commit is False and transaction status is Idle + # then call is_begin_not_required() function to check BEGIN + # is required or not. + + if not trans_obj.auto_commit \ + and conn.transaction_status() == TX_STATUS_IDLE \ + and is_begin_required(sql): + conn.execute_void("BEGIN;") + + # Execute sql asynchronously with params is None + # and formatted_error is True. + status, result = conn.execute_async(sql) + + # If the transaction aborted for some reason and + # Auto RollBack is True then issue a rollback to cleanup. + trans_status = conn.transaction_status() + if trans_status == TX_STATUS_INERROR and trans_obj.auto_rollback: + conn.execute_void("ROLLBACK;") + else: + status = False + result = gettext('Not connected to server or connection with the server has been closed.') + + can_edit = trans_obj.can_edit() + can_filter = trans_obj.can_filter() + + else: + status = False + result = gettext('Either Transaction object or Session object not found.') + + return make_json_response( + data={ + 'status': status, 'result': result, + 'can_edit': can_edit, 'can_filter': can_filter, + 'items_per_page': blueprint.items_per_page.get() + } + ) + + +@blueprint.route('/poll/', methods=["GET"]) +@login_required +def poll(trans_id): + """ + This method polls the result of the asynchronous query and returns the result. + + Args: + trans_id: unique transaction id + """ + col_info = None + primary_keys = None + rows_affected = 0 + + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id) + if status and conn is not None and session_obj is not None: + status, result, col_info = conn.poll() + if status == ASYNC_OK: + status = 'Success' + if 'primary_keys' in session_obj: + primary_keys = session_obj['primary_keys'] + + # if transaction object is instance of QueryToolCommand + # and transaction aborted for some reason then issue a + # rollback to cleanup + if isinstance(trans_obj, QueryToolCommand): + trans_status = conn.transaction_status() + if trans_status == TX_STATUS_INERROR and trans_obj.auto_rollback: + conn.execute_void("ROLLBACK;") + elif status == ASYNC_EXECUTION_ABORTED: + status = 'Cancel' + else: + status = 'Busy' + else: + status = 'NotConnected' + result = error_msg + + # Check column info is available or not + if col_info is not None and len(col_info) > 0: + columns = dict() + rows_affected = conn.rows_affected() + + for col in col_info: + col_type = dict() + col_type['type_code'] = col[1] + col_type['type_name'] = None + columns[col[0]] = col_type + + # As we changed the transaction object we need to + # restore it and update the session variable. + session_obj['columns_info'] = columns + update_session_grid_transaction(trans_id, session_obj) + else: + if result is None: + result = conn.status_message() + rows_affected = conn.rows_affected() + + return make_json_response(data={'status': status, 'result': result, + 'colinfo': col_info, 'primary_keys': primary_keys, + 'rows_affected': rows_affected}) + + +@blueprint.route('/fetch/types/', methods=["GET"]) +@login_required +def fetch_pg_types(trans_id): + """ + This method is used to fetch the pg types, which is required + to map the data type comes as a result of the query. + + Args: + trans_id: unique transaction id + """ + + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id) + if status and conn is not None \ + and trans_obj is not None and session_obj is not None: + + # List of oid for which we need type name from pg_type + oid = '' + for col in session_obj['columns_info']: + type_obj = session_obj['columns_info'][col] + oid += str(type_obj['type_code']) + ',' + + # Remove extra comma + oid = oid[:-1] + status, res = conn.execute_dict( + """SELECT oid, format_type(oid,null) as typname FROM pg_type WHERE oid IN ({0}) ORDER BY oid; +""".format(oid)) + + if status: + # iterate through pg_types and update the type name in session object + for record in res['rows']: + for col in session_obj['columns_info']: + type_obj = session_obj['columns_info'][col] + if type_obj['type_code'] == record['oid']: + type_obj['type_name'] = record['typname'] + + update_session_grid_transaction(trans_id, session_obj) + else: + status = False + res = error_msg + + return make_json_response(data={'status': status, 'result': res}) + + +@blueprint.route('/save/', methods=["PUT", "POST"]) +@login_required +def save(trans_id): + """ + This method is used to save the changes to the server + + Args: + trans_id: unique transaction id + """ + + if request.data: + changed_data = json.loads(request.data.decode()) + else: + changed_data = request.args or request.form + + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id) + if status and conn is not None \ + and trans_obj is not None and session_obj is not None: + + # If there is no primary key found then return from the function. + if len(session_obj['primary_keys']) <= 0 or len(changed_data) <= 0: + return make_json_response( + data={'status': False, + 'result': gettext('No primary key found for this object, so unable to save records.')} + ) + + status, res, query_res = trans_obj.save(changed_data) + else: + status = False + res = error_msg + query_res = None + + return make_json_response(data={'status': status, 'result': res, 'query_result': query_res}) + + +@blueprint.route('/filter/get/', methods=["GET"]) +@login_required +def get_filter(trans_id): + """ + This method is used to get the existing filter. + + Args: + trans_id: unique transaction id + """ + + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id) + if status and conn is not None \ + and trans_obj is not None and session_obj is not None: + + res = trans_obj.get_filter() + else: + status = False + res = error_msg + + return make_json_response(data={'status': status, 'result': res}) + + +@blueprint.route('/filter/apply/', methods=["PUT", "POST"]) +@login_required +def apply_filter(trans_id): + """ + This method is used to apply the filter. + + Args: + trans_id: unique transaction id + """ + if request.data: + filter_sql = json.loads(request.data.decode()) + else: + filter_sql = request.args or request.form + + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id) + if status and conn is not None \ + and trans_obj is not None and session_obj is not None: + + status, res = trans_obj.set_filter(filter_sql) + + # As we changed the transaction object we need to + # restore it and update the session variable. + session_obj['command_obj'] = pickle.dumps(trans_obj, -1) + update_session_grid_transaction(trans_id, session_obj) + else: + status = False + res = error_msg + + return make_json_response(data={'status': status, 'result': res}) + + +@blueprint.route('/filter/inclusive/', methods=["PUT", "POST"]) +@login_required +def append_filter_inclusive(trans_id): + """ + This method is used to append and apply the filter. + + Args: + trans_id: unique transaction id + """ + if request.data: + filter_data = json.loads(request.data.decode()) + else: + filter_data = request.args or request.form + + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id) + if status and conn is not None \ + and trans_obj is not None and session_obj is not None: + + res = None + filter_sql = '' + driver = get_driver(PG_DEFAULT_DRIVER) + + for column_name in filter_data: + column_value = filter_data[column_name] + if column_value is None: + filter_sql = driver.qtIdent(conn, column_name) + ' IS NULL ' + else: + filter_sql = driver.qtIdent(conn, column_name) + ' = ' + driver.qtLiteral(column_value) + + trans_obj.append_filter(filter_sql) + + # As we changed the transaction object we need to + # restore it and update the session variable. + session_obj['command_obj'] = pickle.dumps(trans_obj, -1) + update_session_grid_transaction(trans_id, session_obj) + else: + status = False + res = error_msg + + return make_json_response(data={'status': status, 'result': res}) + + +@blueprint.route('/filter/exclusive/', methods=["PUT", "POST"]) +@login_required +def append_filter_exclusive(trans_id): + """ + This method is used to append and apply the filter. + + Args: + trans_id: unique transaction id + """ + if request.data: + filter_data = json.loads(request.data.decode()) + else: + filter_data = request.args or request.form + + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id) + if status and conn is not None \ + and trans_obj is not None and session_obj is not None: + + res = None + filter_sql = '' + driver = get_driver(PG_DEFAULT_DRIVER) + + for column_name in filter_data: + column_value = filter_data[column_name] + if column_value is None: + filter_sql = driver.qtIdent(conn, column_name) + ' IS NOT NULL ' + else: + filter_sql = driver.qtIdent(conn, column_name) + ' IS DISTINCT FROM ' + driver.qtLiteral(column_value) + + # Call the append_filter method of transaction object + trans_obj.append_filter(filter_sql) + + # As we changed the transaction object we need to + # restore it and update the session variable. + session_obj['command_obj'] = pickle.dumps(trans_obj, -1) + update_session_grid_transaction(trans_id, session_obj) + else: + status = False + res = error_msg + + return make_json_response(data={'status': status, 'result': res}) + + +@blueprint.route('/filter/remove/', methods=["PUT", "POST"]) +@login_required +def remove_filter(trans_id): + """ + This method is used to remove the filter. + + Args: + trans_id: unique transaction id + """ + + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id) + if status and conn is not None \ + and trans_obj is not None and session_obj is not None: + + res = None + + # Call the remove_filter method of transaction object + trans_obj.remove_filter() + + # As we changed the transaction object we need to + # restore it and update the session variable. + session_obj['command_obj'] = pickle.dumps(trans_obj, -1) + update_session_grid_transaction(trans_id, session_obj) + else: + status = False + res = error_msg + + return make_json_response(data={'status': status, 'result': res}) + + +@blueprint.route('/limit/', methods=["PUT", "POST"]) +@login_required +def set_limit(trans_id): + """ + This method is used to set the limit for the SQL. + + Args: + trans_id: unique transaction id + """ + if request.data: + limit = json.loads(request.data.decode()) + else: + limit = request.args or request.form + + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id) + if status and conn is not None \ + and trans_obj is not None and session_obj is not None: + + res = None + + # Call the set_limit method of transaction object + trans_obj.set_limit(limit) + + # As we changed the transaction object we need to + # restore it and update the session variable. + session_obj['command_obj'] = pickle.dumps(trans_obj, -1) + update_session_grid_transaction(trans_id, session_obj) + else: + status = False + res = error_msg + + return make_json_response(data={'status': status, 'result': res}) + + +@blueprint.route('/cancel/', methods=["PUT", "POST"]) +@login_required +def cancel_transaction(trans_id): + """ + This method is used to cancel the running transaction + + Args: + trans_id: unique transaction id + """ + + grid_data = session['gridData'] + + # Return from the function if transaction id not found + if str(trans_id) not in grid_data: + return make_json_response( + data={ + 'status': False, 'result': gettext('Transaction ID not found in the session.') + } + ) + + # Fetch the object for the specified transaction id. + # Use pickle.loads function to get the command object + session_obj = grid_data[str(trans_id)] + trans_obj = pickle.loads(session_obj['command_obj']) + + if trans_obj is not None and session_obj is not None: + + # Fetch the main connection object for the database. + try: + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(trans_obj.sid) + conn = manager.connection(did=trans_obj.did) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + delete_connection = False + + # Connect to the Server if not connected. + if not conn.connected(): + status, msg = conn.connect() + if not status: + return internal_server_error(errormsg=str(msg)) + delete_connection = True + + if conn.connected(): + # on successful connection cancel the running transaction + status, result = conn.cancel_transaction(trans_obj.conn_id, trans_obj.did) + + # Delete connection if we have created it to + # cancel the transaction + if delete_connection: + manager.release(did=trans_obj.did) + else: + status = False + result = gettext('Not connected to server or connection with the server has been closed.') + else: + status = False + result = gettext('Either Transaction object or Session object not found.') + + return make_json_response( + data={ + 'status': status, 'result': result + } + ) + + +@blueprint.route('/object/get/', methods=["GET"]) +@login_required +def get_object_name(trans_id): + """ + This method is used to get the object name + + Args: + trans_id: unique transaction id + """ + + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id) + if status and conn is not None \ + and trans_obj is not None and session_obj is not None: + + res = trans_obj.object_name + else: + status = False + res = error_msg + + return make_json_response(data={'status': status, 'result': res}) + + +@blueprint.route('/auto_commit/', methods=["PUT", "POST"]) +@login_required +def set_auto_commit(trans_id): + """ + This method is used to set the value for auto commit . + + Args: + trans_id: unique transaction id + """ + if request.data: + auto_commit = json.loads(request.data.decode()) + else: + auto_commit = request.args or request.form + + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id) + if status and conn is not None \ + and trans_obj is not None and session_obj is not None: + + res = None + + # Call the set_auto_commit method of transaction object + trans_obj.set_auto_commit(auto_commit) + + # As we changed the transaction object we need to + # restore it and update the session variable. + session_obj['command_obj'] = pickle.dumps(trans_obj, -1) + update_session_grid_transaction(trans_id, session_obj) + else: + status = False + res = error_msg + + return make_json_response(data={'status': status, 'result': res}) + + +@blueprint.route('/auto_rollback/', methods=["PUT", "POST"]) +@login_required +def set_auto_rollback(trans_id): + """ + This method is used to set the value for auto commit . + + Args: + trans_id: unique transaction id + """ + if request.data: + auto_rollback = json.loads(request.data.decode()) + else: + auto_rollback = request.args or request.form + + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id) + if status and conn is not None \ + and trans_obj is not None and session_obj is not None: + + res = None + + # Call the set_auto_rollback method of transaction object + trans_obj.set_auto_rollback(auto_rollback) + + # As we changed the transaction object we need to + # restore it and update the session variable. + session_obj['command_obj'] = pickle.dumps(trans_obj, -1) + update_session_grid_transaction(trans_id, session_obj) + else: + status = False + res = error_msg + + return make_json_response(data={'status': status, 'result': res}) + + +@blueprint.route("/sqleditor.js") +@login_required +def script(): + """render the required javascript""" + return Response(response=render_template("sqleditor/js/sqleditor.js", _=gettext), + status=200, + mimetype="application/javascript") + + +def is_begin_required(query): + word_len = 0 + query = query.strip() + query_len = len(query) + + # Check word length (since "beginx" is not "begin"). + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + # Transaction control commands. These should include every keyword that + # gives rise to a TransactionStmt in the backend grammar, except for the + # savepoint-related commands. + # + # (We assume that START must be START TRANSACTION, since there is + # presently no other "START foo" command.) + + keyword = query[0:word_len] + + if word_len == 5 and keyword.lower() == "abort": + return False + if word_len == 5 and keyword.lower() == "begin": + return False + if word_len == 5 and keyword.lower() == "start": + return False + if word_len == 6 and keyword.lower() == "commit": + return False + if word_len == 3 and keyword.lower() == "end": + return False + if word_len == 8 and keyword.lower() == "rollback": + return False + if word_len == 7 and keyword.lower() == "prepare": + # PREPARE TRANSACTION is a TC command, PREPARE foo is not + query = query[word_len:query_len] + query = query.strip() + query_len = len(query) + word_len = 0 + + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + keyword = query[0:word_len] + if word_len == 11 and keyword.lower() == "transaction": + return False + return True + + # Commands not allowed within transactions. The statements checked for + # here should be exactly those that call PreventTransactionChain() in the + # backend. + if word_len == 6 and keyword.lower() == "vacuum": + return False + + if word_len == 7 and keyword.lower() == "cluster": + # CLUSTER with any arguments is allowed in transactions + query = query[word_len:query_len] + query = query.strip() + + if query[0].isalpha(): + return True # has additional words + return False # it's CLUSTER without arguments + + if word_len == 6 and keyword.lower() == "create": + query = query[word_len:query_len] + query = query.strip() + query_len = len(query) + word_len = 0 + + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + keyword = query[0:word_len] + if word_len == 8 and keyword.lower() == "database": + return False + if word_len == 10 and keyword.lower() == "tablespace": + return False + + # CREATE [UNIQUE] INDEX CONCURRENTLY isn't allowed in xacts + if word_len == 7 and keyword.lower() == "cluster": + query = query[word_len:query_len] + query = query.strip() + query_len = len(query) + word_len = 0 + + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + keyword = query[0:word_len] + + if word_len == 5 and keyword.lower() == "index": + query = query[word_len:query_len] + query = query.strip() + query_len = len(query) + word_len = 0 + + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + keyword = query[0:word_len] + if word_len == 12 and keyword.lower() == "concurrently": + return False + return True + + if word_len == 5 and keyword.lower() == "alter": + query = query[word_len:query_len] + query = query.strip() + query_len = len(query) + word_len = 0 + + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + keyword = query[0:word_len] + + # ALTER SYSTEM isn't allowed in xacts + if word_len == 6 and keyword.lower() == "system": + return False + return True + + # Note: these tests will match DROP SYSTEM and REINDEX TABLESPACE, which + # aren't really valid commands so we don't care much. The other four + # possible matches are correct. + if word_len == 4 and keyword.lower() == "drop" \ + or word_len == 7 and keyword.lower() == "reindex": + query = query[word_len:query_len] + query = query.strip() + query_len = len(query) + word_len = 0 + + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + keyword = query[0:word_len] + if word_len == 8 and keyword.lower() == "database": + return False + if word_len == 6 and keyword.lower() == "system": + return False + if word_len == 10 and keyword.lower() == "tablespace": + return False + return True + + # DISCARD ALL isn't allowed in xacts, but other variants are allowed. + if word_len == 7 and keyword.lower() == "discard": + query = query[word_len:query_len] + query = query.strip() + query_len = len(query) + word_len = 0 + + while (word_len < query_len) and query[word_len].isalpha(): + word_len += 1 + + keyword = query[0:word_len] + if word_len == 3 and keyword.lower() == "all": + return False + return True + + return True diff --git a/web/pgadmin/tools/sqleditor/command.py b/web/pgadmin/tools/sqleditor/command.py new file mode 100644 index 000000000..f5f0eb879 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/command.py @@ -0,0 +1,638 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2016, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +""" Implemented classes for the different object type used by data grid """ + +from abc import ABCMeta, abstractmethod +from flask import render_template +from flask.ext.babel import gettext +from pgadmin.utils.driver import get_driver +from pgadmin.utils.ajax import forbidden +from config import PG_DEFAULT_DRIVER +import six + +VIEW_FIRST_100_ROWS = 1 +VIEW_LAST_100_ROWS = 2 +VIEW_ALL_ROWS = 3 +VIEW_FILTERED_ROWS = 4 + + +class ObjectRegistry(ABCMeta): + """ + class ObjectRegistry(ABCMeta) + Every object will be registered automatically by its object type. + + Class-level Methods: + ----------- ------- + * get_object(cls, name, **kwargs) + - This method returns the object based on register object type + else return not implemented error. + """ + + registry = dict() + + def __init__(cls, name, bases, d): + """ + This method is used to register the objects based on object type. + """ + + if d and 'object_type' in d: + ObjectRegistry.registry[d['object_type']] = cls + + ABCMeta.__init__(cls, name, bases, d) + + @classmethod + def get_object(cls, name, **kwargs): + """ + This method returns the object based on register object type + else return not implemented error + + Args: + name: object type for which object to be returned. + **kwargs: N number of parameters + """ + + if name in ObjectRegistry.registry: + return (ObjectRegistry.registry[name])(**kwargs) + + raise NotImplementedError( + gettext("This feature has not been implemented for object type '{0}'!").format(name) + ) + + +@six.add_metaclass(ObjectRegistry) +class BaseCommand(object): + """ + class BaseCommand + + It is a base class for SQL Tools like data grid and query tool. + A different sql tools must implement this to expose abstract methods. + + Abstract Methods: + -------- ------- + * get_sql() + - This method returns the proper SQL query for the object type. + + * can_edit() + - This method returns True/False, specifying whether data is + editable or not. + + * can_filter() + - This method returns True/False, specifying whether filter + will be applied on data or not. + """ + + def __init__(self, **kwargs): + """ + This method is used to initialize the class and + create a proper object name which will be used + to fetch the data using namespace name and object name. + + Args: + **kwargs : N number of parameters + """ + + # Save the server id and database id, namespace id, object id + self.sid = kwargs['sid'] if 'sid' in kwargs else None + self.did = kwargs['did'] if 'did' in kwargs else None + + @abstractmethod + def get_sql(self): + pass + + @abstractmethod + def can_edit(self): + pass + + @abstractmethod + def can_filter(self): + pass + + +class SQLFilter(object): + """ + class SQLFilter + + Implementation of filter class for sql grid. + + Class-level Methods: + ----------- ------- + * get_filter() + - This method returns the filter applied. + * set_filter(row_filter) + - This method sets the filter to be applied. + * append_filter(row_filter) + - This method is used to append the filter within existing filter + * remove_filter() + - This method removes the filter applied. + * validate_filter(row_filter) + - This method validates the given filter. + """ + + def __init__(self, **kwargs): + """ + This method is used to initialize the class and + create a proper object name which will be used + to fetch the data using namespace name and object name. + + Args: + **kwargs : N number of parameters + """ + # Save the server id and database id, namespace id, object id + assert ('sid' in kwargs) + assert ('did' in kwargs) + assert ('obj_id' in kwargs) + + self.sid = kwargs['sid'] + self.did = kwargs['did'] + self.obj_id = kwargs['obj_id'] + self.__row_filter = kwargs['sql_filter'] if 'sql_filter' in kwargs else None + + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(self.sid) + conn = manager.connection(did=self.did) + + ver = manager.version + # we will set template path for sql scripts + if ver >= 90100: + self.sql_path = 'sqleditor/sql/9.1_plus' + + if conn.connected(): + # Fetch the Namespace Name and object Name + query = render_template("/".join([self.sql_path, 'objectname.sql']), obj_id=self.obj_id) + + status, result = conn.execute_dict(query) + if not status: + raise Exception(result) + + self.nsp_name = result['rows'][0]['nspname'] + self.object_name = result['rows'][0]['relname'] + else: + raise Exception(gettext('Not connected to server or connection with the server has been closed.')) + + def get_filter(self): + """ + This function returns the filter. + """ + return self.__row_filter + + def set_filter(self, row_filter): + """ + This function validates the filter and set the + given filter to member variable. + + Args: + row_filter: sql query + """ + + status, msg = self.validate_filter(row_filter) + + if status: + self.__row_filter = row_filter + + return status, msg + + def is_filter_applied(self): + """ + This function returns True if filter is applied else False. + """ + if self.__row_filter is None or self.__row_filter == '': + return False + + return True + + def remove_filter(self): + """ + This function remove the filter by setting value to None. + """ + self.__row_filter = None + + def append_filter(self, row_filter): + """ + This function will used to get the existing filter and append + the given filter. + + Args: + row_filter: sql query to append + """ + + existing_filter = self.get_filter() + + if existing_filter is None or existing_filter == '': + self.__row_filter = row_filter + else: + self.__row_filter = existing_filter + ' \n AND ' + row_filter + + def validate_filter(self, row_filter): + """ + This function validates the given filter. + + Args: + row_filter: sql syntax to validate + """ + status = True + result = None + + if row_filter is None or row_filter == '': + return False, gettext('Filter string is empty!') + + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(self.sid) + conn = manager.connection(did=self.did) + + if conn.connected(): + sql = render_template("/".join([self.sql_path, 'validate.sql']), + nsp_name=self.nsp_name, object_name=self.object_name, row_filter=row_filter) + + status, result = conn.execute_scalar(sql) + if not status: + result = result.partition("\n")[0] + + return status, result + + +class GridCommand(BaseCommand, SQLFilter): + """ + class GridCommand(object) + + It is a base class for different object type used by data grid. + A different object type must implement this to expose abstract methods. + + Class-level Methods: + ----------- ------- + * get_primary_keys() + - Derived class can implement there own logic to get the primary keys. + + * save() + - Derived class can implement there own logic to save the data into the database. + + * set_limit(limit) + - This method sets the limit for SQL query + + * get_limit() + - This method returns the limit. + """ + + def __init__(self, **kwargs): + """ + This method is used to call base class init to initialize + the data. + + Args: + **kwargs : N number of parameters + """ + BaseCommand.__init__(self, **kwargs) + SQLFilter.__init__(self, **kwargs) + + # Save the connection id, command type + self.conn_id = kwargs['conn_id'] if 'conn_id' in kwargs else None + self.cmd_type = kwargs['cmd_type'] if 'cmd_type' in kwargs else None + self.limit = -1 + + if self.cmd_type == VIEW_FIRST_100_ROWS or self.cmd_type == VIEW_LAST_100_ROWS: + self.limit = 100 + + def get_primary_keys(self): + return None, None + + def save(self, changed_data): + return forbidden(errmsg=gettext("Not allowed to save the data for the selected object!")) + + def get_limit(self): + """ + This function returns the limit for the SQL query. + """ + return self.limit + + def set_limit(self, limit): + """ + This function sets the limit for the SQL query + Args: + limit: limit to be set for SQL. + """ + self.limit = limit + + +class TableCommand(GridCommand): + """ + class TableCommand(GridCommand) + + It is a derived class for Table type. + """ + object_type = 'table' + + def __init__(self, **kwargs): + """ + This method calls the __init__ method of the base class + to get the proper object name. + + Args: + **kwargs : N number of parameters + """ + + # call base class init to fetch the table name + super(TableCommand, self).__init__(**kwargs) + + def get_sql(self): + """ + This method is used to create a proper SQL query + to fetch the data for the specified table + """ + + # Fetch the primary keys for the table + pk_names, primary_keys = self.get_primary_keys() + + sql_filter = self.get_filter() + + if sql_filter is None: + sql = render_template("/".join([self.sql_path, 'objectquery.sql']), object_name=self.object_name, + nsp_name=self.nsp_name, pk_names=pk_names, cmd_type=self.cmd_type, + limit=self.limit) + else: + sql = render_template("/".join([self.sql_path, 'objectquery.sql']), object_name=self.object_name, + nsp_name=self.nsp_name, pk_names=pk_names, cmd_type=self.cmd_type, + sql_filter=sql_filter, limit=self.limit) + + return sql + + def get_primary_keys(self): + """ + This function is used to fetch the primary key columns. + """ + + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(self.sid) + conn = manager.connection(did=self.did, conn_id=self.conn_id) + + pk_names = '' + primary_keys = dict() + + if conn.connected(): + + # Fetch the primary key column names + query = render_template("/".join([self.sql_path, 'primary_keys.sql']), obj_id=self.obj_id) + + status, result = conn.execute_dict(query) + if not status: + raise Exception(result) + + for row in result['rows']: + pk_names += row['attname'] + ',' + primary_keys[row['attname']] = row['typname'] + + if pk_names != '': + # Remove last character from the string + pk_names = pk_names[:-1] + else: + raise Exception(gettext('Not connected to server or connection with the server has been closed.')) + + return pk_names, primary_keys + + def can_edit(self): + return True + + def can_filter(self): + return True + + def save(self, changed_data): + """ + This function is used to save the data into the database. + Depending on condition it will either update or insert the + new row into the database. + + Args: + changed_data: Contains data to be saved + """ + + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(self.sid) + conn = manager.connection(did=self.did, conn_id=self.conn_id) + + status = False + res = None + query_res = dict() + count = 0 + + if conn.connected(): + + # Start the transaction + conn.execute_void('BEGIN;') + + # Iterate total number of records to be updated/inserted + for row in changed_data: + + # if no data to be saved then continue + if 'data' not in row: + continue + + # if 'keys' is present in row then it is and update query + # else it is an insert query. + if 'keys' in row: + # if 'marked_for_deletion' is present in row and it is true then delete + if 'marked_for_deletion' in row and row['marked_for_deletion']: + sql = render_template("/".join([self.sql_path, 'delete.sql']), + primary_keys=row['keys'], object_name=self.object_name, + nsp_name=self.nsp_name) + else: + sql = render_template("/".join([self.sql_path, 'update.sql']), object_name=self.object_name, + data_to_be_saved=row['data'], primary_keys=row['keys'], + nsp_name=self.nsp_name) + else: + sql = render_template("/".join([self.sql_path, 'create.sql']), object_name=self.object_name, + data_to_be_saved=row['data'], nsp_name=self.nsp_name) + + status, res = conn.execute_void(sql) + rows_affected = conn.rows_affected() + + # store the result of each query in dictionary + query_res[count] = {'status': status, 'result': res, + 'sql': sql, 'rows_affected': rows_affected} + count += 1 + + if not status: + conn.execute_void('ROLLBACK;') + # If we roll backed every thing then update the message for + # each sql query. + for val in query_res: + if query_res[val]['status']: + query_res[val]['result'] = 'Transaction ROLLBACK' + + return status, res, query_res + + # Commit the transaction if there is no error found + conn.execute_void('COMMIT;') + + return status, res, query_res + + +class ViewCommand(GridCommand): + """ + class ViewCommand(GridCommand) + + It is a derived class for View type. + """ + object_type = 'view' + + def __init__(self, **kwargs): + """ + This method calls the __init__ method of the base class + to get the proper object name. + + Args: + **kwargs : N number of parameters + """ + + # call base class init to fetch the table name + super(ViewCommand, self).__init__(**kwargs) + + def get_sql(self): + """ + This method is used to create a proper SQL query + to fetch the data for the specified view + """ + sql_filter = self.get_filter() + + if sql_filter is None: + sql = render_template("/".join([self.sql_path, 'objectquery.sql']), object_name=self.object_name, + nsp_name=self.nsp_name, cmd_type=self.cmd_type, + limit=self.limit) + else: + sql = render_template("/".join([self.sql_path, 'objectquery.sql']), object_name=self.object_name, + nsp_name=self.nsp_name, cmd_type=self.cmd_type, + sql_filter=sql_filter, limit=self.limit) + + return sql + + def can_edit(self): + return False + + def can_filter(self): + return True + + +class ForeignTableCommand(GridCommand): + """ + class ForeignTableCommand(GridCommand) + + It is a derived class for ForeignTable type. + """ + object_type = 'foreign-table' + + def __init__(self, **kwargs): + """ + This method calls the __init__ method of the base class + to get the proper object name. + + Args: + **kwargs : N number of parameters + """ + + # call base class init to fetch the table name + super(ForeignTableCommand, self).__init__(**kwargs) + + def get_sql(self): + """ + This method is used to create a proper SQL query + to fetch the data for the specified foreign table + """ + sql_filter = self.get_filter() + + if sql_filter is None: + sql = render_template("/".join([self.sql_path, 'objectquery.sql']), object_name=self.object_name, + nsp_name=self.nsp_name, cmd_type=self.cmd_type, + limit=self.limit) + else: + sql = render_template("/".join([self.sql_path, 'objectquery.sql']), object_name=self.object_name, + nsp_name=self.nsp_name, cmd_type=self.cmd_type, + sql_filter=sql_filter, limit=self.limit) + + return sql + + def can_edit(self): + return False + + def can_filter(self): + return True + + +class CatalogCommand(GridCommand): + """ + class CatalogCommand(GridCommand) + + It is a derived class for CatalogObject type. + """ + object_type = 'catalog_object' + + def __init__(self, **kwargs): + """ + This method calls the __init__ method of the base class + to get the proper object name. + + Args: + **kwargs : N number of parameters + """ + + # call base class init to fetch the table name + super(CatalogCommand, self).__init__(**kwargs) + + def get_sql(self): + """ + This method is used to create a proper SQL query + to fetch the data for the specified catalog object + """ + sql_filter = self.get_filter() + + if sql_filter is None: + sql = render_template("/".join([self.sql_path, 'objectquery.sql']), object_name=self.object_name, + nsp_name=self.nsp_name, cmd_type=self.cmd_type, + limit=self.limit) + else: + sql = render_template("/".join([self.sql_path, 'objectquery.sql']), object_name=self.object_name, + nsp_name=self.nsp_name, cmd_type=self.cmd_type, + sql_filter=sql_filter, limit=self.limit) + + return sql + + def can_edit(self): + return False + + def can_filter(self): + return True + + +class QueryToolCommand(BaseCommand): + """ + class QueryToolCommand(BaseCommand) + + It is a derived class for Query Tool. + """ + object_type = 'query_tool' + + def __init__(self, **kwargs): + # call base class init to fetch the table name + super(QueryToolCommand, self).__init__(**kwargs) + + self.conn_id = None + self.auto_rollback = False + self.auto_commit = True + + def get_sql(self): + return None + + def can_edit(self): + return False + + def can_filter(self): + return False + + def set_connection_id(self, conn_id): + self.conn_id = conn_id + + def set_auto_rollback(self, auto_rollback): + self.auto_rollback = auto_rollback + + def set_auto_commit(self, auto_commit): + self.auto_commit = auto_commit + diff --git a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css new file mode 100644 index 000000000..6d2d626e7 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css @@ -0,0 +1,248 @@ +#main-editor_panel { + height: 100%; + width: 100%; +} + +.sql-editor { + position: absolute; + left: 0; + right: 0; + top : 0; + bottom: 0; +} + +.sql-editor-busy-fetching { + position:absolute; + left: 0; + top: 41px; + bottom: 0; + right: 0; + margin:0; + padding: 0; + background: black; + opacity: 0.4; + z-index: 100; +} + +.sql-editor-busy-icon { + position:absolute; + left: 45%; + top: 40%; +} + +.sql-editor-busy-text { + position:absolute; + left: 42%; + top: 50%; + font-size: 20px; +} + +#editor-panel { + position: absolute; + left: 0; + right: 0; + top : 65px; + bottom: 0; + z-index: 0; +} + +.editor-title { + background-color: #2C76B4; + padding: 2px; + color: white; + font-size: 13px; +} + +.sql-editor-btn-group { + background-color: #D2D2D2; + border: 2px solid #A9A9A9; + left: 0px; + right: 0px; + padding: 2px; +} + +.sql-editor-btn-group button { + padding: 5px; +} + +.sql-editor-btn-group button.dropdown-toggle { + padding-left: 5px !important; + padding-right: 5px !important; +} + +.sql-editor-btn-group .dropdown-menu { + min-width: initial; +} + +.sql-editor-btn-group .backgrid-filter.form-search { + position: relative; + width: 248px; + height: 30px; + float: right; + margin-top: 2px; + margin-right: 10px; +} + +#output-panel .wcDocker { + top: 0px; + bottom: 0px; + height: auto; +} + +#output-panel .wcFrameCenter { + overflow: hidden; +} + +.sql-editor-grid-container { + height: calc(100% - 45px); + overflow: auto; +} + +#datagrid-paginator { + bottom: 0px; + width: 100%; + background-color: white; +} + +/*Move he original checkbox out of the way */ +#datagrid .select-row-cell .sqleditor-checkbox { + position: absolute; + left: -9999px; +} +/*Align the icon and the label.deletable text to same height using tabl-cell display*/ +/*If you change the font-size of the text, you may also want to do som padding or alignhment changes here*/ +#datagrid .sqleditor-checkbox ~ label.deletable > span { + display: table-cell; + vertical-align: middle; + padding-left: 5px; +} +/*The label.deletable will contain the icon and the text, will grab the focus*/ +#datagrid .select-row-cell .sqleditor-checkbox + label.deletable { + cursor: pointer; + display: table; +} +/*The icon container, set it to fixed size and font size, the padding is to align the border*/ +/*If you change the font-size of this icon, be sure to adjust the min-width as well*/ +#datagrid .select-row-cell .sqleditor-checkbox + label.deletable:before { + font-family: 'FontAwesome'; + font-size: small; + font-weight: normal; + display: inline-block; + min-width: 28px; +} + +/* toggle font awesome icon*/ +#datagrid .select-row-cell .sqleditor-checkbox:checked + label:before { + content: "\f014"; +} + +#datagrid .select-row-cell .sqleditor-checkbox:not(:checked) + label:before { + content: "\f014"; +} + +/*Do something on focus, in this case show dashed border*/ +#datagrid .select-row-cell .sqleditor-checkbox:focus + label:before { + border: 1px dashed #777; +} + +/*Do something on hover, in this case change the image color*/ +#datagrid .select-row-cell .sqleditor-checkbox:hover + label:before { + color: #67afe5; +} + +.pgadmin-row-deleted td { + color: red !important; + text-decoration: line-through; +} + +.filter-container { + position: relative; + background-color: white; + border: 1px solid black; + box-shadow: 0.5px 0.5px 5px #000; + padding-bottom: 30px; + top: 10px; + z-index: 1; + margin: auto; + width: 60%; +} + +.filter-container .CodeMirror-scroll { + min-height: 120px; + max-height: 120px; +} + +.filter-container .sql-textarea{ + box-shadow: 0.1px 0.1px 3px #000; + margin-bottom: 5px; +} + +.filter-title { + background-color: #2C76B4; + padding: 2px; + color: white; + font-size: 13px; +} + +#filter .btn-group { + margin-right: 2px; + float: right; +} + +#filter .btn-group > button { + padding: 3px; +} + +#filter .btn-group .btn-primary { + margin: auto !important; +} + +.has-select-all table thead tr th:nth-child(1), +.has-select-all table tbody tr td:nth-child(1) { + width: 35px !important; + max-width: 35px !important; + min-width: 35px !important; +} + +.sql-editor-message { + white-space:pre-wrap; + font-family: monospace; + padding-top: 5px; + padding-left: 10px; +} + +.limit-enabled { + background-color: white; +} + +.sql-editor-history-container { + height: 100%; + overflow: auto; +} + +.sql-status-cell { + max-width: 30px; +} + +.btn-circle { + width: 20px; + height: 20px; + text-align: center; + padding: 0; + font-size: 10px; + line-height: 1.428571429; + border-radius: 10px; +} + +.visibility-hidden { + visibility: hidden; +} + +.sql-editor-mark { + border-bottom: 2px dotted red; +} + +#editor-panel .CodeMirror-activeline-background { + background: #5B9CEF; + color: white; +} diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js b/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js new file mode 100644 index 000000000..d617d7ac8 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js @@ -0,0 +1,2164 @@ +define( + ['jquery', 'underscore', 'alertify', 'pgadmin', 'backbone', 'backgrid', 'codemirror', + 'codemirror/mode/sql', 'codemirror/addon/selection/mark-selection', 'codemirror/addon/selection/active-line', + 'backgrid.select.all', 'backbone.paginator', 'backgrid.paginator', 'backgrid.filter', + 'bootstrap', 'pgadmin.browser', 'wcdocker'], + function($, _, alertify, pgAdmin, Backbone, Backgrid, CodeMirror) { + + // Some scripts do export their object in the window only. + // Generally the one, which do no have AMD support. + var wcDocker = window.wcDocker, + pgBrowser = pgAdmin.Browser; + + /* Return back, this has been called more than once */ + if (pgAdmin.SqlEditor) + return pgAdmin.SqlEditor; + + /* Get the function definition from + * http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript/35302975#35302975 + */ + function epicRandomString(b){for(var a=(Math.random()*eval("1e"+~~(50*Math.random()+50))).toString(36).split(""),c=3;cb)return b?a.substr(a,b):a;a=a.substr(a,b);if(a.length==b)return a;for(;a.length 0) { + _.each(self.primary_keys, function (value, key) { + // Assumption - the data, which are coming will always have data for primary_keys + self.grid_keys[key] = data[key]; + }); + } + + return data; + }, + /* We also need primary key along with the original data, + * which is required to identify this row in the database for modification. + */ + toJSON: function(overridden, keys_only) { + var res = Backbone.Model.prototype.toJSON.apply(this, arguments); + if (!overridden) { + return res; + } + + if (keys_only) + return this.grid_keys; + + return { + 'keys': this.grid_keys, + 'data': res, + 'marked_for_deletion': this.marked_for_deletion + }; + }, + + // This function updates the primary key if changed. + update_keys: function() { + var self = this; + + /* If 'grid_keys' present in the changed object + * then it is an update else insert. + */ + if ('grid_keys' in self) { + /* Iterate through primary keys and check if the key + * is updated or not. If it is updated we need to update + * the grid_keys of the model as well. + */ + _.each(self.primary_keys, function (value, key) { + if (self.grid_keys[key] != self.attributes[key]) + self.grid_keys[key] = self.attributes[key] + }); + } + else { + self.grid_keys = {}; + /* Iterate through primary keys and insert + * the values in models grid_keys. + */ + _.each(self.primary_keys, function (value, key) { + self.grid_keys[key] = self.attributes[key] + }); + } + } + }); + + // Suppose you want to highlight the entire row when an editable field is focused + var FocusableRow = Backgrid.Row.extend({ + highlightColor: "#D9EDF7", + events: { + focusin: "rowFocused", + focusout: "rowLostFocus" + }, + rowFocused: function() { + this.model.trigger('backgrid:row:selected', this); + this.el.style.backgroundColor = this.highlightColor; + }, + rowLostFocus: function() { + this.model.trigger('backgrid:row:deselected', this); + } + }); + + /* + * Extend the FocusableRow row used when user marked + * the row for deletion. + */ + var SqlEditorCustomRow = FocusableRow.extend({ + initialize: function() { + Backgrid.Row.prototype.initialize.apply(this, arguments); + _.bindAll(this, 'render'); + + // Listen for the mark for deletion event and call render method. + this.model.on('backgrid:row:mark:deletion', this.render); + }, + remove: function() { + this.model.off('backgrid:row:mark:deletion', this.render); + Backgrid.Row.prototype.remove.apply(this, arguments); + }, + render: function() { + var res = Backgrid.Row.prototype.render.apply(this, arguments); + + if (this.model.marked_for_deletion) + this.$el.addClass('pgadmin-row-deleted'); + else + this.$el.removeClass('pgadmin-row-deleted'); + + return res; + } + }); + + // Defining Backbone view for the sql grid. + var SQLEditorView = Backbone.View.extend({ + initialize: function(opts) { + this.$el = opts.el; + this.handler = opts.handler; + }, + + // Bind all the events + events: { + "click #btn-save": "on_save", + "click #btn-add-row": "on_add", + "click #btn-filter": "on_show_filter", + "click #btn-include-filter": "on_include_filter", + "click #btn-exclude-filter": "on_exclude_filter", + "click #btn-remove-filter": "on_remove_filter", + "click #btn-apply": "on_apply", + "click #btn-cancel": "on_cancel", + "click #btn-copy": "on_copy", + "click #btn-paste": "on_paste", + "click #btn-flash": "on_flash", + "click #btn-cancel-query": "on_cancel_query", + "click #btn-download": "on_download", + "click #btn-clear": "on_clear", + "click #btn-auto-commit": "on_auto_commit", + "click #btn-auto-rollback": "on_auto_rollback", + "click #btn-clear-history": "on_clear_history", + "change .limit": "on_limit_change" + }, + + /* Defining the template to create buttons and div to render + * the backgrid inside this div. + */ + template: _.template([ + '', + '
', + '', + '
' + ].join("\n")), + + // This function is used to render the template. + render: function() { + var self = this; + + // Render header only. + self.$el.empty(); + self.$el.html(self.template()); + + $('.editor-title').text(self.editor_title); + + var filter = self.$el.find('#sql_filter'); + + self.filter_obj = CodeMirror.fromTextArea(filter.get(0), { + lineNumbers: true, + lineWrapping: true, + matchBrackets: true, + indentUnit: 4, + mode: "text/x-sql" + }); + + // Create main wcDocker instance + var main_docker = new wcDocker( + '#editor-panel', { + allowContextMenu: false, + allowCollapse: false, + themePath: '{{ url_for('static', filename='css/wcDocker/Themes') }}', + theme: 'pgadmin' + }); + + var sub_panel = new pgAdmin.Browser.Panel({ + name: 'sub_panel', + title: false, + width: '100%', + height:'100%', + isCloseable: false, + isPrivate: true + }); + + sub_panel.load(main_docker); + panel = main_docker.addPanel('sub_panel', wcDocker.DOCK.LEFT); + + // Create a Splitter to divide sql code and data output + var hSplitter = new wcSplitter( + "#editor-panel", panel, + wcDocker.ORIENTATION.VERTICAL + ); + hSplitter.scrollable(0, false, false); + hSplitter.scrollable(1, true, true); + + // Initialize this splitter with a layout in each pane. + hSplitter.initLayouts(wcDocker.LAYOUT.SIMPLE, wcDocker.LAYOUT.SIMPLE); + + // By default, the splitter splits down the middle, we split the main panel by 80%. + hSplitter.pos(0.25); + + var text_container = $(''); + + // Add text_container at the top half of the splitter + hSplitter.left().addItem(text_container); + + // Add data output panel at the bottom half of the splitter + var output_container = $('
'); + hSplitter.right().addItem(output_container); + + self.query_tool_obj = CodeMirror.fromTextArea(text_container.get(0), { + lineNumbers: true, + lineWrapping: true, + matchBrackets: true, + indentUnit: 4, + styleSelectedText: true, + mode: "text/x-sql" + }); + + // Create wcDocker for tab set. + var docker = new wcDocker( + '#output-panel', { + allowContextMenu: false, + allowCollapse: false, + themePath: '{{ url_for('static', filename='css/wcDocker/Themes') }}', + theme: 'pgadmin' + }); + + // Create panels for 'Data Output', 'Explain', 'Messages' and 'History' + var data_output = new pgAdmin.Browser.Panel({ + name: 'data_output', + title: '{{ _('Data Output') }}', + width: '100%', + height:'100%', + isCloseable: false, + isPrivate: true, + content: '
' + }) + + var explain = new pgAdmin.Browser.Panel({ + name: 'explain', + title: '{{ _('Explain') }}', + width: '100%', + height:'100%', + isCloseable: false, + isPrivate: true, + content: '
' + }) + + var messages = new pgAdmin.Browser.Panel({ + name: 'messages', + title: '{{ _('Messages') }}', + width: '100%', + height:'100%', + isCloseable: false, + isPrivate: true, + content: '
' + }) + + var history = new pgAdmin.Browser.Panel({ + name: 'history', + title: '{{ _('History') }}', + width: '100%', + height:'100%', + isCloseable: false, + isPrivate: true, + content: '
' + }) + + // Load all the created panels + data_output.load(docker); + explain.load(docker); + messages.load(docker); + history.load(docker); + + // Add all the panels to the docker + self.data_output_panel = docker.addPanel('data_output', wcDocker.DOCK.LEFT); + self.explain_panel = docker.addPanel('explain', wcDocker.DOCK.STACKED, self.data_output_panel); + self.messages_panel = docker.addPanel('messages', wcDocker.DOCK.STACKED, self.data_output_panel); + self.history_panel = docker.addPanel('history', wcDocker.DOCK.STACKED, self.data_output_panel); + + self.render_history_grid(); + }, + + /* This function is responsible to create and render the + * new backgrid and paginator. + */ + render_grid: function(collection, columns) { + var self = this; + + // Remove any existing grid first + if (self.grid) { + self.grid.remove(); + } + + // Remove any existing paginator + if (self.paginator) { + self.paginator.remove(); + } + + // Remove any existing client side filter + if (self.clientSideFilter) { + self.clientSideFilter.remove(); + } + + // Create an array for client filter + var filter_array = new Array() + _.each(columns, function(c) { + filter_array.push(c.name); + }); + + var grid = self.grid = new Backgrid.Grid({ + columns: columns, + collection: collection, + className: "backgrid table-bordered", + row: SqlEditorCustomRow + }), + paginator = self.paginator = new Backgrid.Extension.Paginator({ + goBackFirstOnSort: false, + collection: collection + }), + clientSideFilter = self.clientSideFilter = new Backgrid.Extension.ClientSideFilter({ + collection: collection, + placeholder: _('Search'), + // The model fields to search for matches + fields: filter_array, + // How long to wait after typing has stopped before searching can start + wait: 150 + }); + + // Render the grid + self.$el.find('#datagrid').append(self.grid.render().$el); + + // Render the paginator + self.$el.find('#datagrid-paginator').append(paginator.render().el); + + // Render the client side filter + self.$el.find('.sql-editor-btn-group').append(clientSideFilter.render().el); + + // Forcefully sorting by the first column. + if (columns.length > 1) { + collection.setSorting(columns[1].name); + collection.fullCollection.sort(); + } + }, + + /* This function is responsible to create and render the + * new backgrid for the history tab. + */ + render_history_grid: function() { + var self = this; + + // Remove any existing grid first + if (self.history_grid) { + self.history_grid.remove(); + } + + var history_model = Backbone.Model.extend({ + defaults: { + status: undefined, + start_time: undefined, + query: undefined, + row_affected: 0, + row_retrieved: 0, + total_time: undefined, + message: '' + } + }); + + var history_collection = self.history_collection = new (Backbone.Collection.extend({ + model: history_model, + // comparator to sort the history in reverse order of the start_time + comparator: function(a, b) { + return -a.get('start_time').localeCompare(b.get('start_time')); + } + })); + var columns = [{ + name: "status", + label: "", + cell: Backgrid.Cell.extend({ + class: 'sql-status-cell', + render: function() { + this.$el.empty(); + var $btn = $('', { + class: 'btn btn-circle' + }).appendTo(this.$el); + var $circleDiv = $('', {class: 'fa'}).appendTo($btn); + if (this.model.get('status')) { + $btn.addClass('btn-success'); + $circleDiv.addClass('fa-check'); + } else { + $btn.addClass('btn-danger'); + $circleDiv.addClass('fa-times'); + } + + return this; + }, + editable: false + }), + editable: false + }, { + name: "start_time", + label: "Date", + cell: "string", + editable: false + }, { + name: "query", + label: "Query", + cell: "string", + editable: false + }, { + name: "row_affected", + label: "Rows affected", + cell: "integer", + editable: false + }, { + name: "total_time", + label: "Total Time", + cell: "string", + editable: false + }, { + name: "message", + label: "Message", + cell: "string", + editable: false + }]; + + var grid = self.history_grid = new Backgrid.Grid({ + columns: columns, + collection: history_collection, + className: "backgrid table-bordered" + }); + + // Render the grid + self.$el.find('#history_grid').append(self.history_grid.render().$el); + }, + + // Callback function for Add New Row button click. + on_add: function() { + var self = this; + + // Trigger the addrow signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:addrow', + self, + self.handler + ); + }, + + // Callback function for Save button click. + on_save: function() { + var self = this; + + // Trigger the save signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:save', + self, + self.handler + ); + }, + + // Callback function for filter button click. + on_show_filter: function() { + var self = this; + + // Trigger the show_filter signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:show_filter', + self, + self.handler + ); + }, + + // Callback function for include filter button click. + on_include_filter: function() { + var self = this; + + // Trigger the include_filter signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:include_filter', + self, + self.handler + ); + }, + + // Callback function for exclude filter button click. + on_exclude_filter: function() { + var self = this; + + // Trigger the exclude_filter signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:exclude_filter', + self, + self.handler + ); + }, + + // Callback function for remove filter button click. + on_remove_filter: function() { + var self = this; + + // Trigger the remove_filter signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:remove_filter', + self, + self.handler + ); + }, + + // Callback function for ok button click. + on_apply: function() { + var self = this; + + // Trigger the apply_filter signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:apply_filter', + self, + self.handler + ); + }, + + // Callback function for cancel button click. + on_cancel: function() { + $('#filter').addClass('hidden'); + $('#editor-panel').removeClass('sql-editor-busy-fetching'); + }, + + // Callback function for copy button click. + on_copy: function() { + var self = this; + + // Trigger the copy signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:copy', + self, + self.handler + ); + }, + + // Callback function for paste button click. + on_paste: function() { + var self = this; + + // Trigger the paste signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:paste', + self, + self.handler + ); + }, + + // Callback function for the change event of combo box + on_limit_change: function() { + var self = this; + + // Trigger the limit signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:limit', + self, + self.handler + ); + }, + + // Callback function for the flash button click. + on_flash: function() { + var self = this; + + // Trigger the flash signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:flash', + self, + self.handler + ); + }, + + // Callback function for the cancel query button click. + on_cancel_query: function() { + var self = this; + + // Trigger the cancel-query signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:cancel-query', + self, + self.handler + ); + }, + + // Callback function for the download button click. + on_download: function() { + var self = this; + + // Trigger the download signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:download', + self, + self.handler + ); + }, + + // Callback function for the clear button click. + on_clear: function() { + var self = this; + self.query_tool_obj.setValue(''); + }, + + // Callback function for the clear history button click. + on_clear_history: function() { + var self = this; + + // Remove any existing grid first + if (self.history_grid) { + self.history_collection.reset(); + } + }, + + // Callback function for the auto commit button click. + on_auto_commit: function() { + var self = this; + + // Trigger the auto-commit signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:auto_commit', + self, + self.handler + ); + }, + + // Callback function for the auto rollback button click. + on_auto_rollback: function() { + var self = this; + + // Trigger the download signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:auto_rollback', + self, + self.handler + ); + } + }); + + /* Defining controller class for data grid, which actually + * perform the operations like executing the sql query, poll the result, + * render the data in the grid, Save/Refresh the data etc... + */ + var SqlEditorController = function(container, options) { + this.initialize.apply(this, arguments); + }; + + _.extend( + SqlEditorController.prototype, + Backbone.Events, + { + initialize: function(container, opts) { + this.container = container; + }, + + /* This function is used to create instance of SQLEditorView, + * call the render method of the grid view to render the backgrid + * header and loading icon and start execution of the sql query. + */ + start: function(is_query_tool, editor_title) { + var self = this; + + self.is_query_tool = is_query_tool; + self.items_per_page = 25; + self.rows_affected = 0; + self.marked_line_no = 0; + + // We do not allow to call the start multiple times. + if (self.gridView) + return; + + self.gridView = new SQLEditorView({ + el: self.container, + handler: self + }); + self.transId = self.container.data('transId'); + + self.gridView.editor_title = editor_title; + + // Render the header + self.gridView.render(); + + // Listen on events come from SQLEditorView for the button clicked. + self.on('pgadmin-sqleditor:button:save', self._save, self); + self.on('pgadmin-sqleditor:button:addrow', self._add, self); + self.on('pgadmin-sqleditor:button:show_filter', self._show_filter, self); + self.on('pgadmin-sqleditor:button:include_filter', self._include_filter, self); + self.on('pgadmin-sqleditor:button:exclude_filter', self._exclude_filter, self); + self.on('pgadmin-sqleditor:button:remove_filter', self._remove_filter, self); + self.on('pgadmin-sqleditor:button:apply_filter', self._apply_filter, self); + self.on('pgadmin-sqleditor:button:copy', self._copy, self); + self.on('pgadmin-sqleditor:button:paste', self._paste, self); + self.on('pgadmin-sqleditor:button:limit', self._set_limit, self); + self.on('pgadmin-sqleditor:button:flash', self._refresh, self); + self.on('pgadmin-sqleditor:button:cancel-query', self._cancel_query, self); + self.on('pgadmin-sqleditor:button:download', self._download, self); + self.on('pgadmin-sqleditor:button:auto_rollback', self._auto_rollback, self); + self.on('pgadmin-sqleditor:button:auto_commit', self._auto_commit, self); + + if (self.is_query_tool) { + self.gridView.query_tool_obj.refresh(); + } + else { + self.gridView.query_tool_obj.setOption("readOnly",true); + self.disable_tool_buttons(true); + self._execute_data_query(); + } + }, + + // This function makes the ajax call to execute the sql query. + _execute_data_query: function() { + var self = this; + self.query_start_time = new Date(); + self.rows_affected = 0; + + self.trigger( + 'pgadmin-sqleditor:loading-icon:show', + '{{ _('Initializing the query execution!') }}' + ); + + $.ajax({ + url: "{{ url_for('sqleditor.index') }}" + "view_data/start/" + self.transId, + method: 'GET', + success: function(res) { + if (res.data.status) { + self.trigger( + 'pgadmin-sqleditor:loading-icon:message', + '{{ _('Waiting for the query execution to complete...') }}' + ); + + self.can_edit = res.data.can_edit; + self.can_filter = res.data.can_filter; + self.items_per_page = res.data.items_per_page; + + // Set the sql query to the SQL panel + self.gridView.query_tool_obj.setValue(res.data.sql); + self.query = res.data.sql; + + /* If filter is applied then remove class 'btn-default' + * and add 'btn-warning' to change the colour of the button. + */ + if (self.can_filter && res.data.filter_applied) { + $('#btn-filter').removeClass('btn-default'); + $('#btn-filter-dropdown').removeClass('btn-default'); + $('#btn-filter').addClass('btn-warning'); + $('#btn-filter-dropdown').addClass('btn-warning'); + } + else { + $('#btn-filter').removeClass('btn-warning'); + $('#btn-filter-dropdown').removeClass('btn-warning'); + $('#btn-filter').addClass('btn-default'); + $('#btn-filter-dropdown').addClass('btn-default'); + } + + // Set the combo box value + $(".limit").val(res.data.limit); + + // If status is True then poll the result. + self._poll(); + } + else { + self.update_msg_history(false, res.data.result); + } + }, + error: function(e) { + if (e.readyState == 0) { + self.update_msg_history(false, + '{{ _('Not connected to server Or connection with the server has been closed.') }}' + ); + return; + } + + var msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; + + self.update_msg_history(false, msg); + } + }); + }, + + /* This function makes the ajax call to poll the result, + * if status is Busy then recursively call the poll function + * till the status is 'Success' or 'NotConnected'. If status is + * 'Success' then call the render method to render the data. + */ + _poll: function() { + var self = this; + setTimeout( + function() { + $.ajax({ + url: "{{ url_for('sqleditor.index') }}" + "poll/" + self.transId, + method: 'GET', + success: function(res) { + if (res.data.status === 'Success') { + self.trigger( + 'pgadmin-sqleditor:loading-icon:message', + '{{ _('Loading the data from the database server, and rendering into the grid...') }}' + ); + + self.query_end_time = new Date(); + self.rows_affected = res.data.rows_affected; + + /* If no column information is available it means query + runs successfully with no result to display. In this + case no need to call render function. + */ + if (res.data.colinfo != null) + self._render(res.data); + else { + // Show message in message and history tab in case of query tool + self.total_time = self.get_query_run_time(self.query_start_time, self.query_end_time); + self.update_msg_history(true, res.data.result); + } + + // Enable/Disable query tool button only if is_query_tool is true. + if (self.is_query_tool) { + self.disable_tool_buttons(false); + $("#btn-cancel-query").prop('disabled', true); + } + } + else if (res.data.status === 'Busy') { + // If status is Busy then poll the result by recursive call to the poll function + self._poll(); + } + else if (res.data.status === 'NotConnected') { + + // Enable/Disable query tool button only if is_query_tool is true. + if (self.is_query_tool) { + self.disable_tool_buttons(false); + $("#btn-cancel-query").prop('disabled', true); + } + self.update_msg_history(false, res.data.result); + } + else if (res.data.status === 'Cancel') { + self.update_msg_history(false, "Execution Cancelled!") + } + }, + error: function(e) { + // Enable/Disable query tool button only if is_query_tool is true. + if (self.is_query_tool) { + self.disable_tool_buttons(false); + $("#btn-cancel-query").prop('disabled', true); + } + + if (e.readyState == 0) { + self.update_msg_history(false, + '{{ _('Not connected to server Or connection with the server has been closed.') }}' + ); + return; + } + + var msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; + + self.update_msg_history(false, msg); + } + }); + }, 1); + }, + + /* This function is used to create the backgrid columns, + * create the Backbone PageableCollection and finally render + * the data in the backgrid. + */ + _render: function(data) { + var self = this; + self.colinfo = data.col_info; + self.primary_keys = data.primary_keys; + self.cell_selected = false; + self.selected_model = null; + self.changedModels = []; + + // Stop listening to all the events + if (self.collection) { + self.collection.off('change', self.on_model_change, self); + self.collection.off('backgrid:selected', self.on_backgrid_selected, self); + self.collection.off('backgrid:editing', self.on_cell_editing, self); + } + + /* If object don't have primary keys then set the + * can_edit flag to false. + */ + if (self.primary_keys === null || self.primary_keys === undefined + || _.size(self.primary_keys) === 0) + self.can_edit = false; + else + self.can_edit = true; + + /* If user can edit the data then we should enabled + * Copy Row, Paste Row and 'Add New Row' buttons. + */ + if (self.can_edit) { + $("#btn-add-row").prop('disabled', false); + $("#btn-copy").prop('disabled', false); + $("#btn-paste").prop('disabled', false); + } + + /* If user can filter the data then we should enabled + * Filter and Limit buttons. + */ + if (self.can_filter) { + $(".limit").prop('disabled', false); + $(".limit").addClass('limit-enabled'); + $("#btn-filter").prop('disabled', false); + $("#btn-filter-dropdown").prop('disabled', false); + } + + // Fetch the columns metadata + self.columns = self._fetch_column_metadata(data); + + self.trigger( + 'pgadmin-sqleditor:loading-icon:message', + '{{ _('Loading the data from the database server, and rendering into the grid...') }}', + self + ); + + // Defining backbone's pageable collection. + self.collection = new (Backbone.PageableCollection.extend({ + mode: "client", + state: { + pageSize: self.items_per_page, + order: -1 + }, + model: sqlEditorViewModel.extend({ + primary_keys: self.primary_keys, + + /* Change the idAttribute to random string + * so that it won't clash with id column of + * the table. + */ + idAttribute: epicRandomString(10) + }) + })); + + // Listen on backgrid events + self.collection.on('change', self.on_model_change, self); + self.collection.on('backgrid:selected', self.on_backgrid_selected, self); + self.collection.on('backgrid:editing', self.on_cell_editing, self); + self.collection.on('backgrid:row:selected', self.on_row_selected, self); + self.collection.on('backgrid:row:deselected', self.on_row_deselected, self); + + // Show message in message and history tab in case of query tool + self.total_time = self.get_query_run_time(self.query_start_time, self.query_end_time); + self.update_msg_history(true, "", false); + var message = 'Total query runtime: ' + self.total_time + '\n' + self.rows_affected + ' rows retrieved.'; + $('.sql-editor-message').text(message); + + // Add the data to the collection and render the grid. + self.collection.add(data.result, {parse: true}); + self.gridView.render_grid(self.collection, self.columns); + self.gridView.data_output_panel.focus(); + + // Hide the loading icon + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + }, + + // This function creates the columns as required by the backgrid + _fetch_column_metadata: function(data) { + var colinfo = data.colinfo, + primary_keys = data.primary_keys, + result = data.result, + self = this, + columns = [{ + // name is a required parameter, but you don't really want one on a select all column + name: "", + + /* Extend Backgrid.Extension.SelectRowCell to create + * a custom checkbox where we change the icon when user + * click on checkbox. + */ + cell: Backgrid.Extension.SelectRowCell.extend({ + render: function() { + var self = this, + id = _.uniqueId('sql_grid_deletable_'); + Backgrid.Extension.SelectRowCell.prototype.render.apply(this, arguments); + + // Find the input control and add sqleditor-checkbox class + this.$el.find('input').attr('id', id).addClass( + 'sqleditor-checkbox' + ).prop('checked', this.model.marked_for_deletion); + + // Append the label and add class to change the icon and color using css + this.$el.append( + $('