Hi Dave and Hackers, Please find attached a work-in-progress patch with the following modifications to the query history:
1- Queries generated by pgAdmin in Save Data operations are now recorded in query history. This includes transaction control commands such as 'COMMIT, ROLLBACK, SAVEPOINT, etc.' 2- Queries are now recorded in a correct (mogrified) form after parameters injection - as opposed to older versions < 4.10. They also appear with the correct start time (they used to appear with the start time of the previously executed query - a bug I found). 3- Save Data Queries are visually distinguishable in the query history. 4- Save Data Queries can be shown/hidden from history using a button. 5- Query Tool and View Data now share a common history - this makes more sense now as data changes can be done from both modes. I had a thought to remove the Query History from View Data mode entirely, but I thought it might be useful for some users? I don't know. I am done with all the functionality code, what is left is the design of the toggling button/checkbox. For now, I am using an empty button at the end of the toolbar (next to download button) for experimental purposes. Do you think a button or a checkbox is more appropriate? If a button, I would need a design to use. If a checkbox, I am going to need more help as I am not so good with the design parts. Where should it be placed ( I am thinking above the list of history entries) ? How should it be styled? For now, I will start working on tests and documentation updates for this. Looking forward to your feedback and comments ! P.S I have done a lot of refactoring especially in save_data_changes.py, I would really appreciate it if someone reviewed these changes. Thanks! -- *Yosry Muhammad Yosry* Computer Engineering student, The Faculty of Engineering, Cairo University (2021). Class representative of CMP 2021. https://www.linkedin.com/in/yosrym93/
diff --git a/web/pgadmin/static/js/sqleditor/history/history_collection.js b/web/pgadmin/static/js/sqleditor/history/history_collection.js index a1654fdc..f8380598 100644 --- a/web/pgadmin/static/js/sqleditor/history/history_collection.js +++ b/web/pgadmin/static/js/sqleditor/history/history_collection.js @@ -30,6 +30,10 @@ export default class HistoryCollection { this.onResetHandler(this.historyList); } + toggleSaveDataEntries() { + this.toggleSaveDataEntriesHandler(); + } + onAdd(onAddHandler) { this.onAddHandler = onAddHandler; } @@ -37,4 +41,8 @@ export default class HistoryCollection { onReset(onResetHandler) { this.onResetHandler = onResetHandler; } + + onToggleSaveDataEntries(toggleSaveDataEntriesHandler) { + this.toggleSaveDataEntriesHandler = toggleSaveDataEntriesHandler; + } } diff --git a/web/pgadmin/static/js/sqleditor/history/query_history.js b/web/pgadmin/static/js/sqleditor/history/query_history.js index d0091596..525cfc39 100644 --- a/web/pgadmin/static/js/sqleditor/history/query_history.js +++ b/web/pgadmin/static/js/sqleditor/history/query_history.js @@ -13,6 +13,7 @@ export default class QueryHistory { this.onCopyToEditorHandler = ()=>{}; this.histCollection.onAdd(this.onAddEntry.bind(this)); this.histCollection.onReset(this.onResetEntries.bind(this)); + this.histCollection.onToggleSaveDataEntries(this.onToggleSaveDataEntries.bind(this)); } focus() { @@ -54,6 +55,10 @@ export default class QueryHistory { } } + onToggleSaveDataEntries() { + this.queryHistEntries.toggleSaveDataEntries(); + } + render() { if (this.histCollection.length() == 0) { this.parentNode.empty() diff --git a/web/pgadmin/static/js/sqleditor/history/query_history_details.js b/web/pgadmin/static/js/sqleditor/history/query_history_details.js index afd75101..626e89b5 100644 --- a/web/pgadmin/static/js/sqleditor/history/query_history_details.js +++ b/web/pgadmin/static/js/sqleditor/history/query_history_details.js @@ -96,10 +96,13 @@ export default class QueryHistoryDetails { updateQueryMetaData() { let itemTemplate = (data, description) => { - return `<div class='item'> - <span class='value'>${data}</span> - <span class='description'>${description}</span> - </div>`; + if(data) + return `<div class='item'> + <span class='value'>${data}</span> + <span class='description'>${description}</span> + </div>`; + else + return ''; }; this.$metaData.empty().append( @@ -134,8 +137,23 @@ export default class QueryHistoryDetails { } } + updateInfoMessage() { + if (this.entry.info) { + this.$infoMsgBlock.removeClass('d-none'); + this.$infoMsgBlock.empty().append( + `<div class='history-info-text'> + ${this.entry.info} + </div>` + ); + } else { + this.$infoMsgBlock.addClass('d-none'); + this.$infoMsgBlock.empty(); + } + } + selectiveRender() { this.updateErrorMessage(); + this.updateInfoMessage(); this.updateCopyButton(false); this.updateQueryMetaData(); this.query_codemirror.setValue(this.entry.query); @@ -147,6 +165,7 @@ export default class QueryHistoryDetails { this.parentNode.empty().append( `<div id='query_detail' class='query-detail'> <div class='error-message-block'></div> + <div class='info-message-block'></div> <div class='metadata-block'></div> <div class='query-statement-block'> <div id='history-detail-query'> @@ -168,6 +187,7 @@ export default class QueryHistoryDetails { ); this.$errMsgBlock = this.parentNode.find('.error-message-block'); + this.$infoMsgBlock = this.parentNode.find('.info-message-block'); this.$copyBtn = this.parentNode.find('#history-detail-query .btn-copy'); this.$copyBtn.off('click').on('click', this.copyAllHandler.bind(this)); this.$copyToEditor = this.parentNode.find('#history-detail-query .btn-copy-editor'); diff --git a/web/pgadmin/static/js/sqleditor/history/query_history_entries.js b/web/pgadmin/static/js/sqleditor/history/query_history_entries.js index 51c7847e..9707ae0c 100644 --- a/web/pgadmin/static/js/sqleditor/history/query_history_entries.js +++ b/web/pgadmin/static/js/sqleditor/history/query_history_entries.js @@ -69,7 +69,9 @@ export class QueryHistoryItem { this.$el = $( `<li class='list-item' tabindex='0' data-key='${this.dataKey()}'> <div class='entry ${this.entry.status ? '' : 'error'}'> - <div class='query'>${_.escape(this.entry.query)}</div> + <div class='query'> + ${_.escape(this.entry.query)} + </div> <div class='other-info'> <div class='timestamp'>${this.formatDate(this.entry.start_time)}</div> </div> @@ -80,6 +82,11 @@ export class QueryHistoryItem { .on('click', e => { this.onClickHandler($(e.currentTarget)); }); + + if(this.entry.is_save_data_query) { + this.$el.find('.entry').addClass('save-data-query'); + this.$el.find('.query').prepend('<i class="icon-save-data-changes sql-icon-lg"></i>'); + } } } @@ -221,6 +228,12 @@ export class QueryHistoryEntries { this.setSelectedListItem(newItem.$el); } + toggleSaveDataEntries() { + this.$el.find('.save-data-query').each(function() { + $(this).toggle(); + }); + } + render() { let self = this; self.$el = $(` diff --git a/web/pgadmin/tools/datagrid/templates/datagrid/index.html b/web/pgadmin/tools/datagrid/templates/datagrid/index.html index b1be514d..a69305c5 100644 --- a/web/pgadmin/tools/datagrid/templates/datagrid/index.html +++ b/web/pgadmin/tools/datagrid/templates/datagrid/index.html @@ -348,6 +348,13 @@ <i class="fa fa-download sql-icon-lg" aria-hidden="true"></i> </button> </div> + <div class="btn-group" role="group" aria-label=""> + <button id="btn-history-toggle" type="button" class="btn btn-sm btn-secondary" + title="" + tabindex="0"> + <i class="fa sql-icon-lg" aria-hidden="true"></i> + </button> + </div> </div> <div class="connection_status_wrapper d-flex"> diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index 5091c11d..b862f62c 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -696,7 +696,7 @@ def generate_client_primary_key_name(columns_info): @login_required def save(trans_id): """ - This method is used to save the changes to the server + This method is used to save the data changes to the server Args: trans_id: unique transaction id @@ -746,7 +746,7 @@ def save(trans_id): return make_json_response( data={'status': status, 'result': u"{}".format(msg)} ) - status, res, query_res, _rowid = trans_obj.save( + status, res, query_results, _rowid = trans_obj.save( changed_data, session_obj['columns_info'], session_obj['client_primary_key'], @@ -754,7 +754,7 @@ def save(trans_id): else: status = False res = error_msg - query_res = None + query_results = None _rowid = None transaction_status = conn.transaction_status() @@ -763,7 +763,7 @@ def save(trans_id): data={ 'status': status, 'result': res, - 'query_result': query_res, + 'query_results': query_results, '_rowid': _rowid, 'transaction_status': transaction_status } diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index 4e1302d7..8c1f57f1 100644 --- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js @@ -112,6 +112,7 @@ define('tools.querytool', [ 'click #btn-flash-menu': 'on_flash', 'click #btn-cancel-query': 'on_cancel_query', 'click #btn-download': 'on_download', + 'click #btn-history-toggle': 'on_history_toggle', 'click #btn-clear': 'on_clear', 'click #btn-auto-commit': 'on_auto_commit', 'click #btn-auto-rollback': 'on_auto_rollback', @@ -1361,26 +1362,26 @@ define('tools.querytool', [ }); } - // Make ajax call to get history data except view/edit data - if(self.handler.is_query_tool) { - $.ajax({ - url: url_for('sqleditor.get_query_history', { - 'trans_id': self.handler.transId, - }), - method: 'GET', - contentType: 'application/json', - }) - .done(function(res) { - res.data.result.map((entry) => { - let newEntry = JSON.parse(entry); - newEntry.start_time = new Date(newEntry.start_time); - self.history_collection.add(newEntry); - }); - }) - .fail(function() { - /* history fetch fail should not affect query tool */ + // Make ajax call to get history data + $.ajax({ + url: url_for('sqleditor.get_query_history', { + 'trans_id': self.handler.transId, + }), + method: 'GET', + contentType: 'application/json', + }) + .done(function(res) { + res.data.result.map((entry) => { + let newEntry = JSON.parse(entry); + newEntry.start_time = new Date(newEntry.start_time); + self.history_collection.add(newEntry); }); - } else { + }) + .fail(function() { + /* history fetch fail should not affect query tool */ + }); + + if(!self.is_query_tool) { self.historyComponent.setEditorPref({'copy_to_editor':false}); } }, @@ -1886,6 +1887,16 @@ define('tools.querytool', [ queryToolActions.download(this.handler); }, + on_history_toggle: function() { + var self = this; + + self.handler.trigger( + 'pgadmin-sqleditor:button:toggle-history', + self, + self.handler + ); + }, + keyAction: function(event) { var panel_type=''; @@ -2248,6 +2259,7 @@ define('tools.querytool', [ self.on('pgadmin-sqleditor:button:explain-timing', self._explain_timing, self); self.on('pgadmin-sqleditor:button:explain-summary', self._explain_summary, self); self.on('pgadmin-sqleditor:button:explain-settings', self._explain_settings, self); + self.on('pgadmin-sqleditor:button:toggle-history', self.history_toggle, self); // Indentation related self.on('pgadmin-sqleditor:indent_selected_code', self._indent_selected_code, self); self.on('pgadmin-sqleditor:unindent_selected_code', self._unindent_selected_code, self); @@ -2711,36 +2723,38 @@ define('tools.querytool', [ new Date()); } - let hist_entry = { + let history_entry = { 'status': status, 'start_time': self.query_start_time, 'query': self.query, 'row_affected': self.rows_affected, 'total_time': self.total_time, 'message': msg, + 'is_save_data_query': false, }; - /* Make ajax call to save the history data - * Do not bother query tool if failed to save - * Not applicable for view/edit data - */ - if(self.is_query_tool) { - $.ajax({ - url: url_for('sqleditor.add_query_history', { - 'trans_id': self.transId, - }), - method: 'POST', - contentType: 'application/json', - data: JSON.stringify(hist_entry), - }) - .done(function() {}) - .fail(function() {}); - } - - self.gridView.history_collection.add(hist_entry); + self.add_to_history(history_entry); } }, + /* Make ajax call to save the history data */ + add_to_history: function(history_entry) { + var self = this; + + $.ajax({ + url: url_for('sqleditor.add_query_history', { + 'trans_id': self.transId, + }), + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(history_entry), + }) + .done(function() {}) + .fail(function() {}); + + self.gridView.history_collection.add(history_entry); + }, + /* This function is used to check whether cell * is editable or not depending on primary keys * and staged_rows flag @@ -2901,7 +2915,7 @@ define('tools.querytool', [ var req_data = self.data_store, view = self.gridView; req_data.columns = view ? view.handler.columns : self.columns; - var save_successful = false; + var save_successful = false, save_start_time = new Date(); // Make ajax call to save the data $.ajax({ @@ -2950,7 +2964,7 @@ define('tools.querytool', [ if(is_added) { // Update the rows in a grid after addition dataView.beginUpdate(); - _.each(res.data.query_result, function(r) { + _.each(res.data.query_results, function(r) { if (!_.isNull(r.row_added)) { // Fetch temp_id returned by server after addition var row_id = Object.keys(r.row_added)[0]; @@ -3044,6 +3058,22 @@ define('tools.querytool', [ grid.gotoCell(_row_index, 1); } + var query_history_info_msg = gettext('This query was generated by pgAdmin as part of a "Save Data" operation'); + + _.each(res.data.query_results, function(r) { + var history_entry = { + 'status': r.status, + 'start_time': save_start_time, + 'query': r.sql, + 'row_affected': r.rows_affected, + 'total_time': null, + 'message': r.result, + 'is_save_data_query': true, + 'info': query_history_info_msg, + }; + self.add_to_history(history_entry); + }); + self.trigger('pgadmin-sqleditor:loading-icon:hide'); grid.invalidate(); @@ -3636,7 +3666,6 @@ define('tools.querytool', [ mode_disabled = true; } - $('#btn-clear-dropdown').prop('disabled', mode_disabled); $('#btn-explain').prop('disabled', mode_disabled); $('#btn-explain-analyze').prop('disabled', mode_disabled); $('#btn-explain-options-dropdown').prop('disabled', mode_disabled); @@ -4040,6 +4069,10 @@ define('tools.querytool', [ is_query_running = value; }, + history_toggle: function() { + this.gridView.history_collection.toggleSaveDataEntries(); + }, + /* Checks if there is any unsaved data changes, unsaved changes in the query or uncommitted transactions before closing a panel */ check_needed_confirmations_before_closing_panel: function(is_close_event_call = false) { diff --git a/web/pgadmin/tools/sqleditor/static/scss/_history.scss b/web/pgadmin/tools/sqleditor/static/scss/_history.scss index 37ed8b0e..9005b83a 100644 --- a/web/pgadmin/tools/sqleditor/static/scss/_history.scss +++ b/web/pgadmin/tools/sqleditor/static/scss/_history.scss @@ -111,6 +111,17 @@ } } + .info-message-block { + background: $sql-history-detail-bg; + flex: 0.3; + padding-left: 20px; + + .history-info-text { + @extend .text-12; + padding: 7px 0; + } + } + .metadata-block { flex: 0.4; padding: 10px 20px; diff --git a/web/pgadmin/tools/sqleditor/utils/save_changed_data.py b/web/pgadmin/tools/sqleditor/utils/save_changed_data.py index 935d6591..12df12e4 100644 --- a/web/pgadmin/tools/sqleditor/utils/save_changed_data.py +++ b/web/pgadmin/tools/sqleditor/utils/save_changed_data.py @@ -32,9 +32,7 @@ def save_changed_data(changed_data, columns_info, conn, command_obj, """ status = False res = None - query_res = dict() - count = 0 - list_of_rowid = [] + query_results = [] operations = ('added', 'updated', 'deleted') list_of_sql = {} _rowid = None @@ -44,267 +42,279 @@ def save_changed_data(changed_data, columns_info, conn, command_obj, for col_name, col_info in columns_info.items() } - if conn.connected(): - is_savepoint = False - # Start the transaction if the session is idle - if conn.transaction_status() == TX_STATUS_IDLE: - conn.execute_void('BEGIN;') - else: - conn.execute_void('SAVEPOINT save_data;') - is_savepoint = True - - # Iterate total number of records to be updated/inserted - for of_type in changed_data: - # No need to go further if its not add/update/delete operation - if of_type not in operations: - continue - # if no data to be save then continue - if len(changed_data[of_type]) < 1: - continue - - column_type = {} - column_data = {} - for each_col in columns_info: - if ( - columns_info[each_col]['not_null'] and - not columns_info[each_col]['has_default_val'] - ): - column_data[each_col] = None - column_type[each_col] = \ - columns_info[each_col]['type_name'] - else: - column_type[each_col] = \ - columns_info[each_col]['type_name'] - - # For newly added rows - if of_type == 'added': - # Python dict does not honour the inserted item order - # So to insert data in the order, we need to make ordered - # list of added index We don't need this mechanism in - # updated/deleted rows as it does not matter in - # those operations - added_index = OrderedDict( - sorted( - changed_data['added_index'].items(), - key=lambda x: int(x[0]) - ) + is_savepoint = False + # Start the transaction if the session is idle + if conn.transaction_status() == TX_STATUS_IDLE: + sql = 'BEGIN;' + else: + sql = 'SAVEPOINT save_data;' + is_savepoint = True + + status, res = execute_void_wrapper(conn, sql, query_results) + if not status: + return status, res, query_results, None + + # Iterate total number of records to be updated/inserted + for of_type in changed_data: + # No need to go further if its not add/update/delete operation + if of_type not in operations: + continue + # if no data to be save then continue + if len(changed_data[of_type]) < 1: + continue + + column_type = {} + column_data = {} + for each_col in columns_info: + if ( + columns_info[each_col]['not_null'] and + not columns_info[each_col]['has_default_val'] + ): + column_data[each_col] = None + column_type[each_col] = \ + columns_info[each_col]['type_name'] + else: + column_type[each_col] = \ + columns_info[each_col]['type_name'] + + # For newly added rows + if of_type == 'added': + # Python dict does not honour the inserted item order + # So to insert data in the order, we need to make ordered + # list of added index We don't need this mechanism in + # updated/deleted rows as it does not matter in + # those operations + added_index = OrderedDict( + sorted( + changed_data['added_index'].items(), + key=lambda x: int(x[0]) ) - list_of_sql[of_type] = [] + ) + list_of_sql[of_type] = [] - # When new rows are added, only changed columns data is - # sent from client side. But if column is not_null and has - # no_default_value, set column to blank, instead - # of not null which is set by default. - column_data = {} - pk_names, primary_keys = command_obj.get_primary_keys() - has_oids = 'oid' in column_type - - for each_row in added_index: - # Get the row index to match with the added rows - # dict key - tmp_row_index = added_index[each_row] - data = changed_data[of_type][tmp_row_index]['data'] - # Remove our unique tracking key - data.pop(client_primary_key, None) - data.pop('is_row_copied', None) - list_of_rowid.append(data.get(client_primary_key)) - - # Update columns value with columns having - # not_null=False and has no default value - column_data.update(data) - - sql = render_template( - "/".join([command_obj.sql_path, 'insert.sql']), - data_to_be_saved=column_data, - pgadmin_alias=pgadmin_alias, - primary_keys=None, - object_name=command_obj.object_name, - nsp_name=command_obj.nsp_name, - data_type=column_type, - pk_names=pk_names, - has_oids=has_oids - ) + # When new rows are added, only changed columns data is + # sent from client side. But if column is not_null and has + # no_default_value, set column to blank, instead + # of not null which is set by default. + column_data = {} + pk_names, primary_keys = command_obj.get_primary_keys() + has_oids = 'oid' in column_type + + for each_row in added_index: + # Get the row index to match with the added rows + # dict key + tmp_row_index = added_index[each_row] + data = changed_data[of_type][tmp_row_index]['data'] + # Remove our unique tracking key + data.pop(client_primary_key, None) + data.pop('is_row_copied', None) + + # Update columns value with columns having + # not_null=False and has no default value + column_data.update(data) - select_sql = render_template( - "/".join([command_obj.sql_path, 'select.sql']), - object_name=command_obj.object_name, - nsp_name=command_obj.nsp_name, - primary_keys=primary_keys, - has_oids=has_oids - ) + sql = render_template( + "/".join([command_obj.sql_path, 'insert.sql']), + data_to_be_saved=column_data, + pgadmin_alias=pgadmin_alias, + primary_keys=None, + object_name=command_obj.object_name, + nsp_name=command_obj.nsp_name, + data_type=column_type, + pk_names=pk_names, + has_oids=has_oids + ) - list_of_sql[of_type].append({ - 'sql': sql, 'data': data, - 'client_row': tmp_row_index, - 'select_sql': select_sql - }) - # Reset column data - column_data = {} - - # For updated rows - elif of_type == 'updated': - list_of_sql[of_type] = [] - for each_row in changed_data[of_type]: - data = changed_data[of_type][each_row]['data'] - pk_escaped = { - pk: pk_val.replace('%', '%%') if hasattr( - pk_val, 'replace') else pk_val - for pk, pk_val in - changed_data[of_type][each_row]['primary_keys'].items() - } - sql = render_template( - "/".join([command_obj.sql_path, 'update.sql']), - data_to_be_saved=data, - pgadmin_alias=pgadmin_alias, - primary_keys=pk_escaped, - object_name=command_obj.object_name, - nsp_name=command_obj.nsp_name, - data_type=column_type - ) - list_of_sql[of_type].append({'sql': sql, 'data': data}) - list_of_rowid.append(data.get(client_primary_key)) - - # For deleted rows - elif of_type == 'deleted': - list_of_sql[of_type] = [] - is_first = True - rows_to_delete = [] - keys = None - no_of_keys = None - for each_row in changed_data[of_type]: - rows_to_delete.append(changed_data[of_type][each_row]) - # Fetch the keys for SQL generation - if is_first: - # We need to covert dict_keys to normal list in - # Python3 - # In Python2, it's already a list & We will also - # fetch column names using index - keys = list( - changed_data[of_type][each_row].keys() - ) - no_of_keys = len(keys) - is_first = False - # Map index with column name for each row - for row in rows_to_delete: - for k, v in row.items(): - # Set primary key with label & delete index based - # mapped key - try: - row[changed_data['columns'] - [int(k)]['name']] = v - except ValueError: - continue - del row[k] + select_sql = render_template( + "/".join([command_obj.sql_path, 'select.sql']), + object_name=command_obj.object_name, + nsp_name=command_obj.nsp_name, + primary_keys=primary_keys, + has_oids=has_oids + ) + + list_of_sql[of_type].append({ + 'sql': sql, 'data': data, + 'client_row': tmp_row_index, + 'select_sql': select_sql, + 'row_id': data.get(client_primary_key) + }) + # Reset column data + column_data = {} + # For updated rows + elif of_type == 'updated': + list_of_sql[of_type] = [] + for each_row in changed_data[of_type]: + data = changed_data[of_type][each_row]['data'] + pk_escaped = { + pk: pk_val.replace('%', '%%') if hasattr( + pk_val, 'replace') else pk_val + for pk, pk_val in + changed_data[of_type][each_row]['primary_keys'].items() + } sql = render_template( - "/".join([command_obj.sql_path, 'delete.sql']), - data=rows_to_delete, - primary_key_labels=keys, - no_of_keys=no_of_keys, + "/".join([command_obj.sql_path, 'update.sql']), + data_to_be_saved=data, + pgadmin_alias=pgadmin_alias, + primary_keys=pk_escaped, object_name=command_obj.object_name, - nsp_name=command_obj.nsp_name + nsp_name=command_obj.nsp_name, + data_type=column_type ) - list_of_sql[of_type].append({'sql': sql, 'data': {}}) - - for opr, sqls in list_of_sql.items(): - for item in sqls: - if item['sql']: - item['data'] = { - pgadmin_alias[k] if k in pgadmin_alias else k: v - for k, v in item['data'].items() - } - - row_added = None - - def failure_handle(res): - if is_savepoint: - conn.execute_void('ROLLBACK TO SAVEPOINT ' - 'save_data;') - msg = 'Query ROLLBACK, but the current ' \ - 'transaction is still ongoing.' - else: - conn.execute_void('ROLLBACK;') - msg = 'Transaction 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'] = msg - - # If list is empty set rowid to 1 - try: - if list_of_rowid: - _rowid = list_of_rowid[count] - else: - _rowid = 1 - except Exception: - _rowid = 0 - - return status, res, query_res, _rowid - + list_of_sql[of_type].append({'sql': sql, + 'data': data, + 'row_id': + data.get(client_primary_key)}) + + # For deleted rows + elif of_type == 'deleted': + list_of_sql[of_type] = [] + is_first = True + rows_to_delete = [] + keys = None + no_of_keys = None + for each_row in changed_data[of_type]: + rows_to_delete.append(changed_data[of_type][each_row]) + # Fetch the keys for SQL generation + if is_first: + # We need to covert dict_keys to normal list in + # Python3 + # In Python2, it's already a list & We will also + # fetch column names using index + keys = list( + changed_data[of_type][each_row].keys() + ) + no_of_keys = len(keys) + is_first = False + # Map index with column name for each row + for row in rows_to_delete: + for k, v in row.items(): + # Set primary key with label & delete index based + # mapped key try: - # Fetch oids/primary keys - if 'select_sql' in item and item['select_sql']: - status, res = conn.execute_dict( - item['sql'], item['data']) - else: - status, res = conn.execute_void( - item['sql'], item['data']) - except Exception as _: - failure_handle(res) - raise + row[changed_data['columns'] + [int(k)]['name']] = v + except ValueError: + continue + del row[k] + + sql = render_template( + "/".join([command_obj.sql_path, 'delete.sql']), + data=rows_to_delete, + primary_key_labels=keys, + no_of_keys=no_of_keys, + object_name=command_obj.object_name, + nsp_name=command_obj.nsp_name + ) + list_of_sql[of_type].append({'sql': sql, 'data': {}}) + + def failure_handle(res, row_id): + mogrified_sql = conn.mogrify(item['sql'], item['data']) + mogrified_sql = mogrified_sql if mogrified_sql is not None \ + else item['sql'] + query_results.append({ + 'status': False, + 'result': res, + 'sql': mogrified_sql, + 'rows_affected': 0, + 'row_added': None + }) + + if is_savepoint: + sql = 'ROLLBACK TO SAVEPOINT save_data;' + msg = 'A ROLLBACK was done for the save operation only. ' \ + 'The active transaction is not affected.' + else: + sql = 'ROLLBACK;' + msg = 'A ROLLBACK was done for the save transaction.' + + rollback_status, rollback_result = \ + execute_void_wrapper(conn, sql, query_results) + if not rollback_status: + return rollback_status, rollback_result, query_results, None + + # If we roll backed every thing then update the + # message for each sql query. + for query in query_results: + if query['status']: + query['result'] = msg + + return False, res, query_results, row_id + + for opr, sqls in list_of_sql.items(): + for item in sqls: + if item['sql']: + item['data'] = { + pgadmin_alias[k] if k in pgadmin_alias else k: v + for k, v in item['data'].items() + } + + row_added = None + + try: + # Fetch oids/primary keys + if 'select_sql' in item and item['select_sql']: + status, res = conn.execute_dict( + item['sql'], item['data']) + else: + status, res = conn.execute_void( + item['sql'], item['data']) + except Exception as _: + failure_handle(res, item.get('row_id', 0)) + raise + + if not status: + return failure_handle(res, item.get('row_id', 0)) + + # Select added row from the table + if 'select_sql' in item: + status, sel_res = conn.execute_dict( + item['select_sql'], res['rows'][0]) if not status: - return failure_handle(res) - - # Select added row from the table - if 'select_sql' in item: - status, sel_res = conn.execute_dict( - item['select_sql'], res['rows'][0]) - - if not status: - if is_savepoint: - conn.execute_void('ROLLBACK TO SAVEPOINT' - ' save_data;') - msg = 'Query ROLLBACK, the current' \ - ' transaction is still ongoing.' - else: - conn.execute_void('ROLLBACK;') - msg = 'Transaction 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'] = msg - - # If list is empty set rowid to 1 - try: - if list_of_rowid: - _rowid = list_of_rowid[count] - else: - _rowid = 1 - except Exception: - _rowid = 0 - - return status, sel_res, query_res, _rowid - - if 'rows' in sel_res and len(sel_res['rows']) > 0: - row_added = { - item['client_row']: sel_res['rows'][0]} - - rows_affected = conn.rows_affected() - # store the result of each query in dictionary - query_res[count] = { - 'status': status, - 'result': None if row_added else res, - 'sql': item['sql'], 'rows_affected': rows_affected, - 'row_added': row_added - } - - count += 1 - - # Commit the transaction if no error is found & autocommit is activated - if auto_commit: - conn.execute_void('COMMIT;') - - return status, res, query_res, _rowid + return failure_handle(sel_res, item.get('row_id', 0)) + + if 'rows' in sel_res and len(sel_res['rows']) > 0: + row_added = { + item['client_row']: sel_res['rows'][0]} + + rows_affected = conn.rows_affected() + mogrified_sql = conn.mogrify(item['sql'], item['data']) + mogrified_sql = mogrified_sql if mogrified_sql is not None \ + else item['sql'] + # store the result of each query in dictionary + query_results.append({ + 'status': status, + 'result': None if row_added else res, + 'sql': mogrified_sql, + 'rows_affected': rows_affected, + 'row_added': row_added + }) + + # Commit the transaction if no error is found & autocommit is activated + if auto_commit: + sql = 'COMMIT;' + status, res = execute_void_wrapper(conn, sql, query_results) + if not status: + return status, res, query_results, None + + return status, res, query_results, _rowid + + +def execute_void_wrapper(conn, sql, query_results): + """ + Executes a sql query with no return and adds it to query_results + :param sql: Sql query + :param query_results: A list of query results in the save operation + :return: status, result + """ + status, res = conn.execute_void(sql) + if status: + query_results.append({ + 'status': status, + 'result': res, + 'sql': sql, 'rows_affected': 0, + 'row_added': None + }) + return status, res diff --git a/web/pgadmin/tools/sqleditor/utils/tests/test_save_changed_data.py b/web/pgadmin/tools/sqleditor/utils/tests/test_save_changed_data.py index 8a4a0bd8..f25f2247 100644 --- a/web/pgadmin/tools/sqleditor/utils/tests/test_save_changed_data.py +++ b/web/pgadmin/tools/sqleditor/utils/tests/test_save_changed_data.py @@ -116,8 +116,9 @@ class TestSaveChangedData(BaseTestGenerator): ] }, save_status=False, - check_sql=None, - check_result=None + check_sql= + "SELECT * FROM %s WHERE pk_col = 1 AND normal_col = 'four'", + check_result='SELECT 0' )), ('When updating a row in a valid way', dict( save_payload={ @@ -171,9 +172,9 @@ class TestSaveChangedData(BaseTestGenerator): "updated": { "1": {"err": False, - "data": {"pk_col": "2"}, + "data": {"pk_col": "1"}, "primary_keys": - {"pk_col": 1} + {"pk_col": 2} } }, "added": {}, @@ -210,8 +211,9 @@ class TestSaveChangedData(BaseTestGenerator): ] }, save_status=False, - check_sql=None, - check_result=None + check_sql= + "SELECT * FROM %s WHERE pk_col = 1 AND normal_col = 'two'", + check_result='SELECT 0' )), ('When deleting a row', dict( save_payload={ @@ -283,20 +285,19 @@ class TestSaveChangedData(BaseTestGenerator): save_status = response_data['data']['status'] self.assertEquals(save_status, self.save_status) - if self.check_sql: - # Execute check sql - # Add test table name to the query - check_sql = self.check_sql % self.test_table_name - is_success, response_data = \ - execute_query(tester=self.tester, - query=check_sql, - start_query_tool_url=self.start_query_tool_url, - poll_url=self.poll_url) - self.assertEquals(is_success, True) + # Execute check sql + # Add test table name to the query + check_sql = self.check_sql % self.test_table_name + is_success, response_data = \ + execute_query(tester=self.tester, + query=check_sql, + start_query_tool_url=self.start_query_tool_url, + poll_url=self.poll_url) + self.assertEquals(is_success, True) - # Check table for updates - result = response_data['data']['result'] - self.assertEquals(result, self.check_result) + # Check table for updates + result = response_data['data']['result'] + self.assertEquals(result, self.check_result) def tearDown(self): # Disconnect the database diff --git a/web/pgadmin/utils/driver/psycopg2/connection.py b/web/pgadmin/utils/driver/psycopg2/connection.py index 230e15af..9e6aaa5d 100644 --- a/web/pgadmin/utils/driver/psycopg2/connection.py +++ b/web/pgadmin/utils/driver/psycopg2/connection.py @@ -1917,3 +1917,17 @@ Failed to reset the connection to the server due to following error: ) return enc_password + + def mogrify(self, query, parameters): + """ + This function will return the sql query after parameters binding + :param query: sql query before parameters (variables) binding + :param parameters: query parameters / variables + :return: + """ + status, cursor = self.__cursor() + if not status: + return None + else: + mogrified_sql = cursor.mogrify(query, parameters) + return mogrified_sql