diff --git a/docs/en_US/editgrid.rst b/docs/en_US/editgrid.rst index 1854274d5..b697449d6 100644 --- a/docs/en_US/editgrid.rst +++ b/docs/en_US/editgrid.rst @@ -42,13 +42,15 @@ The top row of the data grid displays the name of each column, the data type, and if applicable, the number of characters allowed. A column that is part of the primary key will additionally be marked with [PK]. +.. _modifying-data-grid: + To modify the displayed data: * To change a numeric value within the grid, double-click the value to select the field. Modify the content in the square in which it is displayed. * To change a non-numeric value within the grid, double-click the content to access the edit bubble. After modifying the contentof the edit bubble, click - the *Save* button to display your changes in the data grid, or *Cancel* to + the *Ok* button to display your changes in the data grid, or *Cancel* to exit the edit bubble without saving. To enter a newline character, click Ctrl-Enter or Shift-Enter. Newline @@ -70,9 +72,7 @@ quotes to the table, you need to escape these quotes, by typing \'\' To delete a row, press the *Delete* toolbar button. A popup will open, asking you to confirm the deletion. -To commit the changes to the server, select the *Save* toolbar button. -Modifications to a row are written to the server automatically when you select -a different row. +To commit the changes to the server, select the *Save Data* toolbar button. **Geometry Data Viewer** diff --git a/docs/en_US/images/query_output_data.png b/docs/en_US/images/query_output_data.png old mode 100755 new mode 100644 index 8eec7087e..6f6f0dc11 Binary files a/docs/en_US/images/query_output_data.png and b/docs/en_US/images/query_output_data.png differ diff --git a/docs/en_US/images/query_tool.png b/docs/en_US/images/query_tool.png old mode 100755 new mode 100644 index b9d165efb..6f6f0dc11 Binary files a/docs/en_US/images/query_tool.png and b/docs/en_US/images/query_tool.png differ diff --git a/docs/en_US/images/query_toolbar.png b/docs/en_US/images/query_toolbar.png index 9ae9cef11..b87c0d72b 100644 Binary files a/docs/en_US/images/query_toolbar.png and b/docs/en_US/images/query_toolbar.png differ diff --git a/docs/en_US/keyboard_shortcuts.rst b/docs/en_US/keyboard_shortcuts.rst index 57a5c2eca..177bdf841 100644 --- a/docs/en_US/keyboard_shortcuts.rst +++ b/docs/en_US/keyboard_shortcuts.rst @@ -154,6 +154,8 @@ When using the Query Tool, the following shortcuts are available: +==========================+====================+===================================+ | F5 | F5 | Execute query | +--------------------------+--------------------+-----------------------------------+ + | F6 | F6 | Save data changes | + +--------------------------+--------------------+-----------------------------------+ | F7 | F7 | EXPLAIN query | +--------------------------+--------------------+-----------------------------------+ | Shift + F7 | Shift + F7 | EXPLAIN ANALYZE query | diff --git a/docs/en_US/preferences.rst b/docs/en_US/preferences.rst index 28ecb6ab5..ab00a45de 100644 --- a/docs/en_US/preferences.rst +++ b/docs/en_US/preferences.rst @@ -320,6 +320,10 @@ Use the fields on the *Options* panel to manage editor preferences. editor will prompt the user to saved unsaved query modifications when exiting the Query Tool. +* When the *Prompt to commit/rollback active transactions?* switch is set to + *True*, the editor will prompt the user to commit or rollback changes when + exiting the Query Tool while the current transaction is not committed. + * Use the *Tab size* field to specify the number of spaces per tab character in the editor. diff --git a/docs/en_US/query_tool.rst b/docs/en_US/query_tool.rst index d9746ddca..973d3b474 100644 --- a/docs/en_US/query_tool.rst +++ b/docs/en_US/query_tool.rst @@ -12,11 +12,15 @@ allows you to: * Issue ad-hoc SQL queries. * Execute arbitrary SQL commands. +* Edit the result set of a SELECT query if it is + :ref:`updatable `. * Displays current connection and transaction status as configured by the user. * Save the data displayed in the output panel to a CSV file. -* Review the execution plan of a SQL statement in either a text or a graphical format. +* Review the execution plan of a SQL statement in either a text or a graphical + format. * View analytical information about a SQL statement. + .. image:: images/query_tool.png :alt: Query tool window :align: center @@ -120,6 +124,28 @@ You can: set query execution options. * Use the *Download as CSV* icon to download the content of the *Data Output* tab as a comma-delimited file. +* Edit the data in the result set of a SELECT query if it is updatable. + +.. _updatable-result-set: + +A result set is updatable if: + +* All the columns belong to the same table. +* All the primary keys of the table are selected. +* No columns are duplicated. + +An updatable result set can be modified just like in +:ref:`View/Edit Data ` mode. + +If Auto-commit is off, the data changes are made as part of the ongoing +transaction, if no transaction is ongoing a new one is initiated. The data +changes are not committed to the database unless the transaction is committed. + +If any errors occur during saving (for example, trying to save NULL into a +column with NOT NULL constraint) the data changes are rolled back to an +automatically created SAVEPOINT to ensure any previously executed queries in +the ongoing transaction are not rolled back. + All rowsets from previous queries or commands that are displayed in the *Data Output* panel will be discarded when you invoke another query; open another diff --git a/docs/en_US/query_tool_toolbar.rst b/docs/en_US/query_tool_toolbar.rst index 3ce9deeb3..2a2b5beae 100644 --- a/docs/en_US/query_tool_toolbar.rst +++ b/docs/en_US/query_tool_toolbar.rst @@ -31,7 +31,7 @@ File Options +======================+===================================================================================================+================+ | *Open File* | Click the *Open File* icon to display a previously saved query in the SQL Editor. | Accesskey + O | +----------------------+---------------------------------------------------------------------------------------------------+----------------+ - | *Save* | Click the *Save* icon to perform a quick-save of a previously saved query, or to access the | Accesskey + S | + | *Save File* | Click the *Save* icon to perform a quick-save of a previously saved query, or to access the | Accesskey + S | | | *Save* menu: | | | | | | | | * Select *Save* to save the selected content of the SQL Editor panel in a file. | | @@ -50,6 +50,8 @@ Editing Options +----------------------+---------------------------------------------------------------------------------------------------+----------------+ | Icon | Behavior | Shortcut | +======================+===================================================================================================+================+ + | *Save Data* | Click the *Save Data* icon to save data changes in the Data Output Panel to the server. | F6 | + +----------------------+---------------------------------------------------------------------------------------------------+----------------+ | *Find* | Use the *Find* menu to search, replace, or navigate the code displayed in the SQL Editor: | | | +---------------------------------------------------------------------------------------------------+----------------+ | | Select *Find* to provide a search target, and search the SQL Editor contents. | Cmd+F | @@ -67,11 +69,10 @@ Editing Options | | Select *Jump* to navigate to the next occurrence of the search target. | Alt+G | +----------------------+---------------------------------------------------------------------------------------------------+----------------+ | *Copy* | Click the *Copy* icon to copy the content that is currently highlighted in the Data Output panel. | Accesskey + C | - | | when in View/Edit data mode. | | +----------------------+---------------------------------------------------------------------------------------------------+----------------+ - | *Paste* | Click the *Paste* icon to paste a previously row into a new row when in View/Edit data mode. | Accesskey + P | + | *Paste* | Click the *Paste* icon to paste a previously row into a new row. | Accesskey + P | +----------------------+---------------------------------------------------------------------------------------------------+----------------+ - | *Delete* | Click the *Delete* icon to delete the selected rows when in View/Edit data mode. | Accesskey + D | + | *Delete* | Click the *Delete* icon to delete the selected rows. | Accesskey + D | +----------------------+---------------------------------------------------------------------------------------------------+----------------+ | *Edit* | Use options on the *Edit* menu to access text editing tools; the options operate on the text | | | | displayed in the SQL Editor panel when in Query Tool mode: | | diff --git a/docs/en_US/release_notes_4_11.rst b/docs/en_US/release_notes_4_11.rst index 6e0b5b06e..ba1aa862e 100644 --- a/docs/en_US/release_notes_4_11.rst +++ b/docs/en_US/release_notes_4_11.rst @@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o New features ************ +| `Issue #1760 `_ - Add support for editing of resultsets in the Query Tool, if the data can be identified as updatable. | `Issue #4335 `_ - Add EXPLAIN options for SETTINGS and SUMMARY. | `Issue #4318 `_ - Set the mouse cursor appropriately based on the layout lock state. diff --git a/web/pgadmin/feature_tests/file_manager_test.py b/web/pgadmin/feature_tests/file_manager_test.py index 1c74d40b6..b6b35c047 100644 --- a/web/pgadmin/feature_tests/file_manager_test.py +++ b/web/pgadmin/feature_tests/file_manager_test.py @@ -65,7 +65,8 @@ class CheckFileManagerFeatureTest(BaseFeatureTest): self.page.open_query_tool() def _create_new_file(self): - self.page.find_by_css_selector(QueryToolLocatorsCss.btn_save).click() + self.page.find_by_css_selector(QueryToolLocatorsCss.btn_save_file)\ + .click() # Set the XSS value in input self.page.find_by_css_selector('.change_file_types') self.page.fill_input_by_css_selector("input#file-input-path", diff --git a/web/pgadmin/feature_tests/locators.py b/web/pgadmin/feature_tests/locators.py index 3b5a549de..2bac3708e 100644 --- a/web/pgadmin/feature_tests/locators.py +++ b/web/pgadmin/feature_tests/locators.py @@ -1,5 +1,5 @@ class QueryToolLocatorsCss: - btn_save = "#btn-save" + btn_save_file = "#btn-save-file" btn_execute_query = "#btn-flash" btn_query_dropdown = "#btn-query-dropdown" btn_auto_rollback = "#btn-auto-rollback" diff --git a/web/pgadmin/feature_tests/query_tool_journey_test.py b/web/pgadmin/feature_tests/query_tool_journey_test.py index 79f6b7dba..6d41fd1cf 100644 --- a/web/pgadmin/feature_tests/query_tool_journey_test.py +++ b/web/pgadmin/feature_tests/query_tool_journey_test.py @@ -7,6 +7,7 @@ # ########################################################################## +import sys import pyperclip import random @@ -28,11 +29,24 @@ class QueryToolJourneyTest(BaseFeatureTest): ] test_table_name = "" + test_editable_table_name = "" def before(self): self.test_table_name = "test_table" + str(random.randint(1000, 3000)) test_utils.create_table( self.server, self.test_db, self.test_table_name) + + self.test_editable_table_name = "test_editable_table" + \ + str(random.randint(1000, 3000)) + create_sql = ''' + CREATE TABLE "%s" ( + pk_column NUMERIC PRIMARY KEY, + normal_column NUMERIC + ); + ''' % self.test_editable_table_name + test_utils.create_table_with_query( + self.server, self.test_db, create_sql) + self.page.add_server(self.server) def runTest(self): @@ -40,9 +54,21 @@ class QueryToolJourneyTest(BaseFeatureTest): self._execute_query( "SELECT * FROM %s ORDER BY value " % self.test_table_name) + print("Copy rows...", file=sys.stderr, end="") self._test_copies_rows() + print(" OK.", file=sys.stderr) + + print("Copy columns...", file=sys.stderr, end="") self._test_copies_columns() + print(" OK.", file=sys.stderr) + + print("History tab...", file=sys.stderr, end="") self._test_history_tab() + print(" OK.", file=sys.stderr) + + print("Updatable resultsets...", file=sys.stderr, end="") + self._test_updatable_resultset() + print(" OK.", file=sys.stderr) def _test_copies_rows(self): pyperclip.copy("old clipboard contents") @@ -162,6 +188,27 @@ class QueryToolJourneyTest(BaseFeatureTest): .perform() self._assert_clickable(query_we_need_to_scroll_to) + def _test_updatable_resultset(self): + self.page.click_tab("Query Editor") + + # Insert data into test table + self.__clear_query_tool() + self._execute_query( + "INSERT INTO %s VALUES (1, 1), (2, 2);" + % self.test_editable_table_name + ) + + # Select all data (contains the primary key -> should be editable) + self.__clear_query_tool() + query = "SELECT pk_column, normal_column FROM %s" \ + % self.test_editable_table_name + self._check_query_results_editable(query, True) + + # Select data without primary keys -> should not be editable + self.__clear_query_tool() + query = "SELECT normal_column FROM %s" % self.test_editable_table_name + self._check_query_results_editable(query, False) + def __clear_query_tool(self): self.page.click_element( self.page.find_by_xpath("//*[@id='btn-clear-dropdown']") @@ -179,6 +226,7 @@ class QueryToolJourneyTest(BaseFeatureTest): self.page.toggle_open_tree_item('Databases') self.page.toggle_open_tree_item(self.test_db) self.page.open_query_tool() + self.page.wait_for_spinner_to_disappear() def _execute_query(self, query): self.page.fill_codemirror_area_with(query) @@ -188,6 +236,33 @@ class QueryToolJourneyTest(BaseFeatureTest): def _assert_clickable(self, element): self.page.click_element(element) + def _check_query_results_editable(self, query, should_be_editable): + self._execute_query(query) + self.page.wait_for_spinner_to_disappear() + + # Check if the first cell in the first row is editable + is_editable = self._check_cell_editable(1) + self.assertEqual(is_editable, should_be_editable) + # Check that new rows cannot be added + can_add_rows = self._check_can_add_row() + self.assertEqual(can_add_rows, should_be_editable) + + def _check_cell_editable(self, cell_index): + xpath = '//div[contains(@class, "slick-cell") and ' \ + 'contains(@class, "r' + str(cell_index) + '")]' + cell_el = self.page.find_by_xpath(xpath) + cell_classes = cell_el.get_attribute('class') + cell_classes = cell_classes.split(" ") + self.assertFalse('editable' in cell_classes) + ActionChains(self.driver).double_click(cell_el).perform() + cell_classes = cell_el.get_attribute('class') + cell_classes = cell_classes.split(" ") + return 'editable' in cell_classes + + def _check_can_add_row(self): + return self.page.check_if_element_exist_by_xpath( + '//div[contains(@class, "new-row")]') + def after(self): self.page.close_query_tool() self.page.remove_server(self.server) diff --git a/web/pgadmin/feature_tests/view_data_dml_queries.py b/web/pgadmin/feature_tests/view_data_dml_queries.py index e47145e2b..7de93792a 100644 --- a/web/pgadmin/feature_tests/view_data_dml_queries.py +++ b/web/pgadmin/feature_tests/view_data_dml_queries.py @@ -304,7 +304,7 @@ CREATE TABLE public.nonintpkey ) time.sleep(0.2) self._update_cell(cell_xpath, data[str(idx)]) - self.page.find_by_id("btn-save").click() # Save data + self.page.find_by_id("btn-save-data").click() # Save data # There should be some delay after save button is clicked, as it # takes some time to complete save ajax call otherwise discard unsaved # changes dialog will appear if we try to execute query before previous diff --git a/web/pgadmin/static/js/keyboard_shortcuts.js b/web/pgadmin/static/js/keyboard_shortcuts.js index 101aff5b4..b0c92634c 100644 --- a/web/pgadmin/static/js/keyboard_shortcuts.js +++ b/web/pgadmin/static/js/keyboard_shortcuts.js @@ -205,6 +205,7 @@ function keyboardShortcutsQueryTool( let toggleCaseKeys = sqlEditorController.preferences.toggle_case; let commitKeys = sqlEditorController.preferences.commit_transaction; let rollbackKeys = sqlEditorController.preferences.rollback_transaction; + let saveDataKeys = sqlEditorController.preferences.save_data; if (this.validateShortcutKeys(executeKeys, event)) { this._stopEventPropagation(event); @@ -233,6 +234,9 @@ function keyboardShortcutsQueryTool( this._stopEventPropagation(event); queryToolActions.executeRollback(sqlEditorController); } + } else if (this.validateShortcutKeys(saveDataKeys, event)) { + this._stopEventPropagation(event); + queryToolActions.saveDataChanges(sqlEditorController); } else if (( (this.isMac() && event.metaKey) || (!this.isMac() && event.ctrlKey) diff --git a/web/pgadmin/static/js/sqleditor/call_render_after_poll.js b/web/pgadmin/static/js/sqleditor/call_render_after_poll.js index 3f32d5712..57d60537c 100644 --- a/web/pgadmin/static/js/sqleditor/call_render_after_poll.js +++ b/web/pgadmin/static/js/sqleditor/call_render_after_poll.js @@ -37,7 +37,8 @@ export function callRenderAfterPoll(sqlEditor, alertify, res) { const msg = sprintf( gettext('Query returned successfully in %s.'), sqlEditor.total_time); res.result += '\n\n' + msg; - sqlEditor.update_msg_history(true, res.result, false); + sqlEditor.update_msg_history(true, res.result, true); + sqlEditor.reset_data_store(); if (isNotificationEnabled(sqlEditor)) { alertify.success(msg, sqlEditor.info_notifier_timeout); } diff --git a/web/pgadmin/static/js/sqleditor/execute_query.js b/web/pgadmin/static/js/sqleditor/execute_query.js index 11cd84b91..26da69148 100644 --- a/web/pgadmin/static/js/sqleditor/execute_query.js +++ b/web/pgadmin/static/js/sqleditor/execute_query.js @@ -70,6 +70,8 @@ class ExecuteQuery { let httpMessageData = result.data; self.removeGridViewMarker(); + self.updateSqlEditorLastTransactionStatus(httpMessageData.data.transaction_status); + if (ExecuteQuery.isSqlCorrect(httpMessageData)) { self.loadingScreen.setMessage('Waiting for the query to complete...'); @@ -118,6 +120,8 @@ class ExecuteQuery { }) ).then( (httpMessage) => { + self.updateSqlEditorLastTransactionStatus(httpMessage.data.data.transaction_status); + // Enable/Disable commit and rollback button. if (httpMessage.data.data.transaction_status == 2 || httpMessage.data.data.transaction_status == 3) { self.enableTransactionButtons(); @@ -126,6 +130,10 @@ class ExecuteQuery { } if (ExecuteQuery.isQueryFinished(httpMessage)) { + if (this.sqlServerObject.close_on_idle_transaction && + httpMessage.data.data.transaction_status == 0) + this.sqlServerObject.check_needed_confirmations_before_closing_panel(); + self.loadingScreen.setMessage('Loading data from the database server and rendering...'); self.sqlServerObject.call_render_after_poll(httpMessage.data.data); @@ -296,6 +304,10 @@ class ExecuteQuery { this.sqlServerObject.info_notifier_timeout = messageData.info_notifier_timeout; } + updateSqlEditorLastTransactionStatus(transactionStatus) { + this.sqlServerObject.last_transaction_status = transactionStatus; + } + static isSqlCorrect(httpMessageData) { return httpMessageData.data.status; } diff --git a/web/pgadmin/static/js/sqleditor/query_tool_actions.js b/web/pgadmin/static/js/sqleditor/query_tool_actions.js index d3f6d9e17..18e15ecbe 100644 --- a/web/pgadmin/static/js/sqleditor/query_tool_actions.js +++ b/web/pgadmin/static/js/sqleditor/query_tool_actions.js @@ -156,6 +156,11 @@ let queryToolActions = { sqlEditorController.special_sql = 'ROLLBACK;'; self.executeQuery(sqlEditorController); }, + + saveDataChanges: function (sqlEditorController) { + sqlEditorController.close_on_save = false; + sqlEditorController.save_data(); + }, }; module.exports = queryToolActions; diff --git a/web/pgadmin/static/js/sqleditor/query_tool_preferences.js b/web/pgadmin/static/js/sqleditor/query_tool_preferences.js index 7fd19b40d..491f0ca96 100644 --- a/web/pgadmin/static/js/sqleditor/query_tool_preferences.js +++ b/web/pgadmin/static/js/sqleditor/query_tool_preferences.js @@ -29,7 +29,7 @@ function updateUIPreferences(sqlEditor) { .attr('title', shortcut_accesskey_title('Open File',preferences.btn_open_file)) .attr('accesskey', shortcut_key(preferences.btn_open_file)); - $el.find('#btn-save') + $el.find('#btn-save-file') .attr('title', shortcut_accesskey_title('Save File',preferences.btn_save_file)) .attr('accesskey', shortcut_key(preferences.btn_save_file)); @@ -97,6 +97,10 @@ function updateUIPreferences(sqlEditor) { .attr('title', shortcut_title('Download as CSV',preferences.download_csv)); + $el.find('#btn-save-data') + .attr('title', + shortcut_title('Save Data Changes',preferences.save_data)); + $el.find('#btn-commit') .attr('title', shortcut_title('Commit',preferences.commit_transaction)); diff --git a/web/pgadmin/static/scss/_alertify.overrides.scss b/web/pgadmin/static/scss/_alertify.overrides.scss index 413e09e7f..d43becd54 100644 --- a/web/pgadmin/static/scss/_alertify.overrides.scss +++ b/web/pgadmin/static/scss/_alertify.overrides.scss @@ -56,7 +56,7 @@ bottom: $footer-height-calc !important; } .ajs-wrap-text { - word-break: break-all; + word-break: normal; word-wrap: break-word; } /* Removes padding from alertify footer */ diff --git a/web/pgadmin/tools/datagrid/templates/datagrid/index.html b/web/pgadmin/tools/datagrid/templates/datagrid/index.html index 65b6c4ecd..b1be514d7 100644 --- a/web/pgadmin/tools/datagrid/templates/datagrid/index.html +++ b/web/pgadmin/tools/datagrid/templates/datagrid/index.html @@ -21,7 +21,7 @@ tabindex="0"> - +