mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Add support in query history to show internal queries generated by pgAdmin during save data operations. Fixes #4612
This commit is contained in:
committed by
Akshay Joshi
parent
4403f326e9
commit
687204771c
BIN
docs/en_US/images/query_output_history.png
Executable file → Normal file
BIN
docs/en_US/images/query_output_history.png
Executable file → Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 127 KiB |
@@ -211,6 +211,10 @@ The Query History tab displays information about recent commands:
|
|||||||
* The amount of time it took the server to process the query and return a
|
* The amount of time it took the server to process the query and return a
|
||||||
result set.
|
result set.
|
||||||
* Messages returned by the server (not noted on the *Messages* tab).
|
* Messages returned by the server (not noted on the *Messages* tab).
|
||||||
|
* The source of the query (indicated by icons corresponding to the toolbar).
|
||||||
|
|
||||||
|
You can show or hide the queries generated internally by pgAdmin (during
|
||||||
|
'View/Edit Data' or 'Save Data' operations).
|
||||||
|
|
||||||
To erase the content of the *Query History* tab, select *Clear history* from
|
To erase the content of the *Query History* tab, select *Clear history* from
|
||||||
the *Clear* drop-down menu.
|
the *Clear* drop-down menu.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ New features
|
|||||||
| `Issue #4566 <https://redmine.postgresql.org/issues/4566>`_ - Allow enhanced cookie protection to be disabled for compatibility with dynamically addressed hosting environments.
|
| `Issue #4566 <https://redmine.postgresql.org/issues/4566>`_ - Allow enhanced cookie protection to be disabled for compatibility with dynamically addressed hosting environments.
|
||||||
| `Issue #4570 <https://redmine.postgresql.org/issues/4570>`_ - Add an optimisation to the internal code responsible for searching for treeview nodes.
|
| `Issue #4570 <https://redmine.postgresql.org/issues/4570>`_ - Add an optimisation to the internal code responsible for searching for treeview nodes.
|
||||||
| `Issue #4574 <https://redmine.postgresql.org/issues/4574>`_ - Display the row count in the popup message when counting table rows, not just in the properties list.
|
| `Issue #4574 <https://redmine.postgresql.org/issues/4574>`_ - Display the row count in the popup message when counting table rows, not just in the properties list.
|
||||||
|
| `Issue #4612 <https://redmine.postgresql.org/issues/4612>`_ - Add support in query history to show internal queries generated by pgAdmin during save data operations.
|
||||||
|
|
||||||
Housekeeping
|
Housekeeping
|
||||||
************
|
************
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
class QueryToolLocatorsCss:
|
class QueryToolLocatorsCss:
|
||||||
btn_save_file = "#btn-save-file"
|
btn_save_file = "#btn-save-file"
|
||||||
|
btn_save_data = "#btn-save-data"
|
||||||
btn_execute_query = "#btn-flash"
|
btn_execute_query = "#btn-flash"
|
||||||
btn_query_dropdown = "#btn-query-dropdown"
|
btn_query_dropdown = "#btn-query-dropdown"
|
||||||
btn_auto_rollback = "#btn-auto-rollback"
|
btn_auto_rollback = "#btn-auto-rollback"
|
||||||
@@ -16,8 +17,16 @@ class QueryToolLocatorsCss:
|
|||||||
btn_explain_timing = "#btn-explain-timing"
|
btn_explain_timing = "#btn-explain-timing"
|
||||||
btn_clear_dropdown = "#btn-clear-dropdown"
|
btn_clear_dropdown = "#btn-clear-dropdown"
|
||||||
btn_clear = "#btn-clear"
|
btn_clear = "#btn-clear"
|
||||||
|
btn_commit = "#btn-commit"
|
||||||
query_editor_panel = "#output-panel"
|
query_editor_panel = "#output-panel"
|
||||||
query_history_selected = "#query_list .selected"
|
query_history_selected = "#query_list .selected"
|
||||||
|
query_history_selected_icon = '#query_list .selected #query_source_icon'
|
||||||
query_history_detail = "#query_detail"
|
query_history_detail = "#query_detail"
|
||||||
|
query_history_generated_queries_toggle = '#generated-queries-toggle'
|
||||||
editor_panel = "#output-panel"
|
editor_panel = "#output-panel"
|
||||||
query_messages_panel = ".sql-editor-message"
|
query_messages_panel = ".sql-editor-message"
|
||||||
|
execute_icon = "fa-bolt"
|
||||||
|
explain_icon = "fa-hand-pointer-o"
|
||||||
|
explain_analyze_icon = "fa-list-alt"
|
||||||
|
save_data_icon = "icon-save-data-changes"
|
||||||
|
commit_icon = "icon-commit"
|
||||||
|
|||||||
@@ -70,6 +70,14 @@ class QueryToolJourneyTest(BaseFeatureTest):
|
|||||||
self._test_history_tab()
|
self._test_history_tab()
|
||||||
print(" OK.", file=sys.stderr)
|
print(" OK.", file=sys.stderr)
|
||||||
|
|
||||||
|
# Insert data into test editable table
|
||||||
|
self._insert_data_into_test_editable_table()
|
||||||
|
|
||||||
|
print("History query sources and generated queries toggle...",
|
||||||
|
file=sys.stderr, end="")
|
||||||
|
self._test_query_sources_and_generated_queries()
|
||||||
|
print(" OK.", file=sys.stderr)
|
||||||
|
|
||||||
print("Updatable resultsets...", file=sys.stderr, end="")
|
print("Updatable resultsets...", file=sys.stderr, end="")
|
||||||
self._test_updatable_resultset()
|
self._test_updatable_resultset()
|
||||||
print(" OK.", file=sys.stderr)
|
print(" OK.", file=sys.stderr)
|
||||||
@@ -173,24 +181,49 @@ class QueryToolJourneyTest(BaseFeatureTest):
|
|||||||
|
|
||||||
self._assert_clickable(query_we_need_to_scroll_to)
|
self._assert_clickable(query_we_need_to_scroll_to)
|
||||||
|
|
||||||
self.page.click_tab("Query Editor")
|
def _test_query_sources_and_generated_queries(self):
|
||||||
self.__clear_query_tool()
|
self.__clear_query_history()
|
||||||
self.page.click_element(editor_input)
|
self._test_history_query_sources()
|
||||||
self.page.fill_codemirror_area_with("SELECT * FROM hats")
|
self._test_toggle_generated_queries()
|
||||||
for _ in range(15):
|
|
||||||
self.page.find_by_css_selector(
|
|
||||||
QueryToolLocatorsCss.btn_execute_query).click()
|
|
||||||
self.page.wait_for_query_tool_loading_indicator_to_disappear()
|
|
||||||
|
|
||||||
self.page.click_tab("History")
|
def _test_history_query_sources(self):
|
||||||
query_we_need_to_scroll_to = self.page.find_by_xpath(
|
self.page.click_tab("Query Editor")
|
||||||
"//*[@id='query_list']/div/ul/li[17]"
|
self._execute_sources_test_queries()
|
||||||
|
|
||||||
|
self.page.click_tab("Query History")
|
||||||
|
|
||||||
|
history_entries_icons = [
|
||||||
|
QueryToolLocatorsCss.commit_icon,
|
||||||
|
QueryToolLocatorsCss.save_data_icon,
|
||||||
|
QueryToolLocatorsCss.save_data_icon,
|
||||||
|
QueryToolLocatorsCss.execute_icon,
|
||||||
|
QueryToolLocatorsCss.explain_analyze_icon,
|
||||||
|
QueryToolLocatorsCss.explain_icon
|
||||||
|
]
|
||||||
|
|
||||||
|
history_entries_queries = [
|
||||||
|
"COMMIT;",
|
||||||
|
"UPDATE public.%s SET normal_column = '10'::numeric "
|
||||||
|
"WHERE pk_column = '1';" % self.test_editable_table_name,
|
||||||
|
"BEGIN;",
|
||||||
|
"SELECT * FROM %s" % self.test_editable_table_name,
|
||||||
|
"SELECT * FROM %s" % self.test_editable_table_name,
|
||||||
|
"SELECT * FROM %s" % self.test_editable_table_name
|
||||||
|
]
|
||||||
|
|
||||||
|
self._check_history_queries_and_icons(history_entries_queries,
|
||||||
|
history_entries_icons)
|
||||||
|
|
||||||
|
def _test_toggle_generated_queries(self):
|
||||||
|
xpath = '//li[contains(@class, "pgadmin-query-history-entry")]'
|
||||||
|
self.assertTrue(self.page.check_if_element_exist_by_xpath(xpath))
|
||||||
|
toggle_el = self.page.find_by_xpath(
|
||||||
|
'//input[@id ="generated-queries-toggle"]/..'
|
||||||
)
|
)
|
||||||
for _ in range(17):
|
toggle_el.click()
|
||||||
ActionChains(self.page.driver) \
|
self.assertFalse(self.page.check_if_element_exist_by_xpath(xpath))
|
||||||
.send_keys(Keys.ARROW_DOWN) \
|
toggle_el.click()
|
||||||
.perform()
|
self.assertTrue(self.page.check_if_element_exist_by_xpath(xpath))
|
||||||
self._assert_clickable(query_we_need_to_scroll_to)
|
|
||||||
|
|
||||||
def _test_updatable_resultset(self):
|
def _test_updatable_resultset(self):
|
||||||
if self.driver_version < 2.8:
|
if self.driver_version < 2.8:
|
||||||
@@ -198,13 +231,6 @@ class QueryToolJourneyTest(BaseFeatureTest):
|
|||||||
|
|
||||||
self.page.click_tab("Query Editor")
|
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)
|
# Select all data (contains the primary key -> should be editable)
|
||||||
self.__clear_query_tool()
|
self.__clear_query_tool()
|
||||||
query = "SELECT pk_column, normal_column FROM %s" \
|
query = "SELECT pk_column, normal_column FROM %s" \
|
||||||
@@ -216,6 +242,85 @@ class QueryToolJourneyTest(BaseFeatureTest):
|
|||||||
query = "SELECT normal_column FROM %s" % self.test_editable_table_name
|
query = "SELECT normal_column FROM %s" % self.test_editable_table_name
|
||||||
self._check_query_results_editable(query, False)
|
self._check_query_results_editable(query, False)
|
||||||
|
|
||||||
|
def _execute_sources_test_queries(self):
|
||||||
|
self.__clear_query_tool()
|
||||||
|
|
||||||
|
self._explain_query(
|
||||||
|
"SELECT * FROM %s;"
|
||||||
|
% self.test_editable_table_name
|
||||||
|
)
|
||||||
|
self._explain_analyze_query(
|
||||||
|
"SELECT * FROM %s;"
|
||||||
|
% self.test_editable_table_name
|
||||||
|
)
|
||||||
|
self._execute_query(
|
||||||
|
"SELECT * FROM %s;"
|
||||||
|
% self.test_editable_table_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# Turn off autocommit
|
||||||
|
query_options = self.page.find_by_css_selector(
|
||||||
|
QueryToolLocatorsCss.btn_query_dropdown)
|
||||||
|
query_options.click()
|
||||||
|
self.page.find_by_css_selector(
|
||||||
|
QueryToolLocatorsCss.btn_auto_commit).click()
|
||||||
|
query_options.click() # Click again to close dropdown
|
||||||
|
|
||||||
|
self._update_numeric_cell(2, 10)
|
||||||
|
|
||||||
|
self._commit_transaction()
|
||||||
|
|
||||||
|
# Turn on autocommit
|
||||||
|
query_options = self.page.find_by_css_selector(
|
||||||
|
QueryToolLocatorsCss.btn_query_dropdown)
|
||||||
|
query_options.click()
|
||||||
|
self.page.find_by_css_selector(
|
||||||
|
QueryToolLocatorsCss.btn_auto_commit).click()
|
||||||
|
query_options.click() # Click again to close dropdown
|
||||||
|
|
||||||
|
def _check_history_queries_and_icons(self, history_queries, history_icons):
|
||||||
|
# Select first query history entry
|
||||||
|
self.page.find_by_xpath("//*[@id='query_list']/div/ul/li[1]").click()
|
||||||
|
for icon, query in zip(history_icons, history_queries):
|
||||||
|
# Check query
|
||||||
|
query_history_selected_item = self.page.find_by_css_selector(
|
||||||
|
QueryToolLocatorsCss.query_history_selected
|
||||||
|
)
|
||||||
|
self.assertIn(query, query_history_selected_item.text)
|
||||||
|
# Check source icon
|
||||||
|
query_history_selected_icon = self.page.find_by_css_selector(
|
||||||
|
QueryToolLocatorsCss.query_history_selected_icon)
|
||||||
|
icon_classes = query_history_selected_icon.get_attribute('class')
|
||||||
|
icon_classes = icon_classes.split(" ")
|
||||||
|
self.assertTrue(icon in icon_classes)
|
||||||
|
# Move to next entry
|
||||||
|
ActionChains(self.page.driver) \
|
||||||
|
.send_keys(Keys.ARROW_DOWN) \
|
||||||
|
.perform()
|
||||||
|
|
||||||
|
def _update_numeric_cell(self, cell_index, value):
|
||||||
|
"""
|
||||||
|
Updates a numeric cell in the first row of the resultset
|
||||||
|
"""
|
||||||
|
xpath = '//div[contains(@class, "slick-row") and ' \
|
||||||
|
'contains(@style, "top:0px")]'
|
||||||
|
xpath += '/div[contains(@class, "slick-cell") and ' \
|
||||||
|
'contains(@class, "r' + str(cell_index) + '")]'
|
||||||
|
cell_el = self.page.find_by_xpath(xpath)
|
||||||
|
ActionChains(self.driver).double_click(cell_el).perform()
|
||||||
|
ActionChains(self.driver).send_keys(value). \
|
||||||
|
send_keys(Keys.ENTER).perform()
|
||||||
|
self.page.find_by_css_selector(
|
||||||
|
QueryToolLocatorsCss.btn_save_data).click()
|
||||||
|
|
||||||
|
def _insert_data_into_test_editable_table(self):
|
||||||
|
self.page.click_tab("Query Editor")
|
||||||
|
self.__clear_query_tool()
|
||||||
|
self._execute_query(
|
||||||
|
"INSERT INTO %s VALUES (1, 1), (2, 2);"
|
||||||
|
% self.test_editable_table_name
|
||||||
|
)
|
||||||
|
|
||||||
def __clear_query_tool(self):
|
def __clear_query_tool(self):
|
||||||
self.page.click_element(
|
self.page.click_element(
|
||||||
self.page.find_by_xpath("//*[@id='btn-clear-dropdown']")
|
self.page.find_by_xpath("//*[@id='btn-clear-dropdown']")
|
||||||
@@ -228,6 +333,19 @@ class QueryToolJourneyTest(BaseFeatureTest):
|
|||||||
)
|
)
|
||||||
self.page.click_modal('Yes')
|
self.page.click_modal('Yes')
|
||||||
|
|
||||||
|
def __clear_query_history(self):
|
||||||
|
self.page.click_element(
|
||||||
|
self.page.find_by_xpath("//*[@id='btn-clear-dropdown']")
|
||||||
|
)
|
||||||
|
ActionChains(self.driver)\
|
||||||
|
.move_to_element(
|
||||||
|
self.page.find_by_xpath(
|
||||||
|
"//*[@id='btn-clear-history']")).perform()
|
||||||
|
self.page.click_element(
|
||||||
|
self.page.find_by_xpath("//*[@id='btn-clear-history']")
|
||||||
|
)
|
||||||
|
self.page.click_modal('Yes')
|
||||||
|
|
||||||
def _navigate_to_query_tool(self):
|
def _navigate_to_query_tool(self):
|
||||||
self.page.toggle_open_tree_item(self.server['name'])
|
self.page.toggle_open_tree_item(self.server['name'])
|
||||||
self.page.toggle_open_tree_item('Databases')
|
self.page.toggle_open_tree_item('Databases')
|
||||||
@@ -240,35 +358,48 @@ class QueryToolJourneyTest(BaseFeatureTest):
|
|||||||
self.page.find_by_css_selector(
|
self.page.find_by_css_selector(
|
||||||
QueryToolLocatorsCss.btn_execute_query).click()
|
QueryToolLocatorsCss.btn_execute_query).click()
|
||||||
|
|
||||||
|
def _explain_query(self, query):
|
||||||
|
self.page.fill_codemirror_area_with(query)
|
||||||
|
self.page.find_by_css_selector(
|
||||||
|
QueryToolLocatorsCss.btn_explain).click()
|
||||||
|
|
||||||
|
def _explain_analyze_query(self, query):
|
||||||
|
self.page.fill_codemirror_area_with(query)
|
||||||
|
self.page.find_by_css_selector(
|
||||||
|
QueryToolLocatorsCss.btn_explain_analyze).click()
|
||||||
|
|
||||||
|
def _commit_transaction(self):
|
||||||
|
self.page.find_by_css_selector(
|
||||||
|
QueryToolLocatorsCss.btn_commit).click()
|
||||||
|
|
||||||
def _assert_clickable(self, element):
|
def _assert_clickable(self, element):
|
||||||
self.page.click_element(element)
|
self.page.click_element(element)
|
||||||
|
|
||||||
def _check_query_results_editable(self, query, should_be_editable):
|
def _check_query_results_editable(self, query, should_be_editable):
|
||||||
self._execute_query(query)
|
self._execute_query(query)
|
||||||
self.page.wait_for_spinner_to_disappear()
|
self.page.wait_for_spinner_to_disappear()
|
||||||
|
|
||||||
# Check if the first cell in the first row is editable
|
# Check if the first cell in the first row is editable
|
||||||
is_editable = self._check_cell_editable(1)
|
is_editable = self._check_cell_editable(1)
|
||||||
self.assertEqual(is_editable, should_be_editable)
|
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):
|
def _check_cell_editable(self, cell_index):
|
||||||
xpath = '//div[contains(@class, "slick-cell") and ' \
|
"""
|
||||||
'contains(@class, "r' + str(cell_index) + '")]'
|
Checks if a cell in the first row of the resultset is editable
|
||||||
|
"""
|
||||||
|
xpath = '//div[contains(@class, "slick-row") and ' \
|
||||||
|
'contains(@style, "top:0px")]'
|
||||||
|
xpath += '/div[contains(@class, "slick-cell") and ' \
|
||||||
|
'contains(@class, "r' + str(cell_index) + '")]'
|
||||||
cell_el = self.page.find_by_xpath(xpath)
|
cell_el = self.page.find_by_xpath(xpath)
|
||||||
cell_classes = cell_el.get_attribute('class')
|
# Get existing value
|
||||||
cell_classes = cell_classes.split(" ")
|
cell_value = int(cell_el.text)
|
||||||
self.assertFalse('editable' in cell_classes)
|
new_value = cell_value + 1
|
||||||
|
# Try to update value
|
||||||
ActionChains(self.driver).double_click(cell_el).perform()
|
ActionChains(self.driver).double_click(cell_el).perform()
|
||||||
cell_classes = cell_el.get_attribute('class')
|
ActionChains(self.driver).send_keys(new_value). \
|
||||||
cell_classes = cell_classes.split(" ")
|
send_keys(Keys.ENTER).perform()
|
||||||
return 'editable' in cell_classes
|
# Check if the value was updated
|
||||||
|
return int(cell_el.text) == new_value
|
||||||
def _check_can_add_row(self):
|
|
||||||
return self.page.check_if_element_exist_by_xpath(
|
|
||||||
'//div[contains(@class, "new-row")]')
|
|
||||||
|
|
||||||
def after(self):
|
def after(self):
|
||||||
self.page.close_query_tool()
|
self.page.close_query_tool()
|
||||||
|
|||||||
@@ -304,7 +304,8 @@ CREATE TABLE public.nonintpkey
|
|||||||
)
|
)
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
self._update_cell(cell_xpath, data[str(idx)])
|
self._update_cell(cell_xpath, data[str(idx)])
|
||||||
self.page.find_by_id("btn-save-data").click() # Save data
|
self.page.find_by_css_selector(
|
||||||
|
QueryToolLocatorsCss.btn_save_data).click()
|
||||||
# There should be some delay after save button is clicked, as it
|
# There should be some delay after save button is clicked, as it
|
||||||
# takes some time to complete save ajax call otherwise discard unsaved
|
# takes some time to complete save ajax call otherwise discard unsaved
|
||||||
# changes dialog will appear if we try to execute query before previous
|
# changes dialog will appear if we try to execute query before previous
|
||||||
|
|||||||
@@ -96,10 +96,13 @@ export default class QueryHistoryDetails {
|
|||||||
|
|
||||||
updateQueryMetaData() {
|
updateQueryMetaData() {
|
||||||
let itemTemplate = (data, description) => {
|
let itemTemplate = (data, description) => {
|
||||||
return `<div class='item'>
|
if(data)
|
||||||
<span class='value'>${data}</span>
|
return `<div class='item'>
|
||||||
<span class='description'>${description}</span>
|
<span class='value'>${data}</span>
|
||||||
</div>`;
|
<span class='description'>${description}</span>
|
||||||
|
</div>`;
|
||||||
|
else
|
||||||
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$metaData.empty().append(
|
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() {
|
selectiveRender() {
|
||||||
this.updateErrorMessage();
|
this.updateErrorMessage();
|
||||||
|
this.updateInfoMessage();
|
||||||
this.updateCopyButton(false);
|
this.updateCopyButton(false);
|
||||||
this.updateQueryMetaData();
|
this.updateQueryMetaData();
|
||||||
this.query_codemirror.setValue(this.entry.query);
|
this.query_codemirror.setValue(this.entry.query);
|
||||||
@@ -147,6 +165,7 @@ export default class QueryHistoryDetails {
|
|||||||
this.parentNode.empty().append(
|
this.parentNode.empty().append(
|
||||||
`<div id='query_detail' class='query-detail'>
|
`<div id='query_detail' class='query-detail'>
|
||||||
<div class='error-message-block'></div>
|
<div class='error-message-block'></div>
|
||||||
|
<div class='info-message-block'></div>
|
||||||
<div class='metadata-block'></div>
|
<div class='metadata-block'></div>
|
||||||
<div class='query-statement-block'>
|
<div class='query-statement-block'>
|
||||||
<div id='history-detail-query'>
|
<div id='history-detail-query'>
|
||||||
@@ -168,6 +187,7 @@ export default class QueryHistoryDetails {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.$errMsgBlock = this.parentNode.find('.error-message-block');
|
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 = this.parentNode.find('#history-detail-query .btn-copy');
|
||||||
this.$copyBtn.off('click').on('click', this.copyAllHandler.bind(this));
|
this.$copyBtn.off('click').on('click', this.copyAllHandler.bind(this));
|
||||||
this.$copyToEditor = this.parentNode.find('#history-detail-query .btn-copy-editor');
|
this.$copyToEditor = this.parentNode.find('#history-detail-query .btn-copy-editor');
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import _ from 'underscore';
|
import _ from 'underscore';
|
||||||
|
import 'bootstrap.toggle';
|
||||||
|
|
||||||
const ARROWUP = 38;
|
const ARROWUP = 38;
|
||||||
const ARROWDOWN = 40;
|
const ARROWDOWN = 40;
|
||||||
@@ -65,11 +66,14 @@ export class QueryHistoryItem {
|
|||||||
return this.formatDate(this.entry.start_time);
|
return this.formatDate(this.entry.start_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(is_pgadmin_queries_shown) {
|
||||||
this.$el = $(
|
this.$el = $(
|
||||||
`<li class='list-item' tabindex='0' data-key='${this.dataKey()}'>
|
`<li class='list-item' tabindex='0' data-key='${this.dataKey()}'>
|
||||||
<div class='entry ${this.entry.status ? '' : 'error'}'>
|
<div class='entry ${this.entry.status ? '' : 'error'}'>
|
||||||
<div class='query'>${_.escape(this.entry.query)}</div>
|
<div class='query'>
|
||||||
|
<i id="query_source_icon" class="query-history-icon sql-icon-lg"></i>
|
||||||
|
${_.escape(this.entry.query)}
|
||||||
|
</div>
|
||||||
<div class='other-info'>
|
<div class='other-info'>
|
||||||
<div class='timestamp'>${this.formatDate(this.entry.start_time)}</div>
|
<div class='timestamp'>${this.formatDate(this.entry.start_time)}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,6 +84,16 @@ export class QueryHistoryItem {
|
|||||||
.on('click', e => {
|
.on('click', e => {
|
||||||
this.onClickHandler($(e.currentTarget));
|
this.onClickHandler($(e.currentTarget));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let query_source = this.entry.query_source;
|
||||||
|
if(query_source)
|
||||||
|
this.$el.find('#query_source_icon').addClass(query_source.ICON_CSS_CLASS);
|
||||||
|
|
||||||
|
if(this.entry.is_pgadmin_query) {
|
||||||
|
this.$el.addClass('pgadmin-query-history-entry');
|
||||||
|
if(!is_pgadmin_queries_shown)
|
||||||
|
this.$el.addClass('d-none');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +104,7 @@ export class QueryHistoryEntries {
|
|||||||
this.groupKeyFormat = 'YYYY MM DD';
|
this.groupKeyFormat = 'YYYY MM DD';
|
||||||
|
|
||||||
this.$el = null;
|
this.$el = null;
|
||||||
|
this.is_pgadmin_queries_shown = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectedChange(onSelectedChangeHandler) {
|
onSelectedChange(onSelectedChangeHandler) {
|
||||||
@@ -98,10 +113,10 @@ export class QueryHistoryEntries {
|
|||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
if (!this.$selectedItem) {
|
if (!this.$selectedItem) {
|
||||||
this.setSelectedListItem(this.$el.find('.list-item').first());
|
this.setSelectedListItem(this.$entriesEl.find('.list-item').first());
|
||||||
}
|
}
|
||||||
this.$selectedItem.trigger('click');
|
this.$selectedItem.trigger('click');
|
||||||
this.$el[0].focus();
|
this.$entriesEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
isArrowDown(event) {
|
isArrowDown(event) {
|
||||||
@@ -175,7 +190,7 @@ export class QueryHistoryEntries {
|
|||||||
|
|
||||||
addEntry(entry) {
|
addEntry(entry) {
|
||||||
/* Add the entry in respective date group in descending sorted order. */
|
/* Add the entry in respective date group in descending sorted order. */
|
||||||
let groups = this.$el.find('.query-group');
|
let groups = this.$entriesEl.find('.query-group');
|
||||||
let groupsKeys = $.map(groups, group => {
|
let groupsKeys = $.map(groups, group => {
|
||||||
return $(group).attr('data-key');
|
return $(group).attr('data-key');
|
||||||
});
|
});
|
||||||
@@ -189,7 +204,7 @@ export class QueryHistoryEntries {
|
|||||||
entry.start_time,
|
entry.start_time,
|
||||||
entryGroupKey
|
entryGroupKey
|
||||||
).render();
|
).render();
|
||||||
this.$el.prepend($groupEl);
|
this.$entriesEl.prepend($groupEl);
|
||||||
} else if (groupIdx < 0 && groups.length != 0) {
|
} else if (groupIdx < 0 && groups.length != 0) {
|
||||||
/* if groups are present, but this is a new group */
|
/* if groups are present, but this is a new group */
|
||||||
$groupEl = new QueryHistoryEntryDateGroup(
|
$groupEl = new QueryHistoryEntryDateGroup(
|
||||||
@@ -206,7 +221,7 @@ export class QueryHistoryEntries {
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
if(i == groupsKeys.length) {
|
if(i == groupsKeys.length) {
|
||||||
this.$el.append($groupEl);
|
this.$entriesEl.append($groupEl);
|
||||||
}
|
}
|
||||||
} else if (groupIdx >= 0) {
|
} else if (groupIdx >= 0) {
|
||||||
/* if the group is present */
|
/* if the group is present */
|
||||||
@@ -215,18 +230,46 @@ export class QueryHistoryEntries {
|
|||||||
|
|
||||||
let newItem = new QueryHistoryItem(entry);
|
let newItem = new QueryHistoryItem(entry);
|
||||||
newItem.onClick(this.setSelectedListItem.bind(this));
|
newItem.onClick(this.setSelectedListItem.bind(this));
|
||||||
newItem.render();
|
newItem.render(this.is_pgadmin_queries_shown);
|
||||||
|
|
||||||
$groupEl.find('.query-entries').prepend(newItem.$el);
|
$groupEl.find('.query-entries').prepend(newItem.$el);
|
||||||
this.setSelectedListItem(newItem.$el);
|
this.setSelectedListItem(newItem.$el);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleGeneratedQueries() {
|
||||||
|
this.$el.find('.pgadmin-query-history-entry').each(function() {
|
||||||
|
$(this).toggleClass('d-none');
|
||||||
|
});
|
||||||
|
this.is_pgadmin_queries_shown = !this.is_pgadmin_queries_shown;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
self.$el = $(`
|
self.$el = $(`
|
||||||
<div id='query_list' class='query-history' tabindex='0'>
|
<div class="toggle-and-history-container">
|
||||||
|
<div class="query-history-toggle">
|
||||||
|
<label class="control-label">
|
||||||
|
Show queries generated internally by pgAdmin?
|
||||||
|
</label>
|
||||||
|
<input id="generated-queries-toggle" type="checkbox"
|
||||||
|
class="pgadmin-controls" data-style="quick"
|
||||||
|
data-size="mini" data-on="Yes" data-off="No"
|
||||||
|
data-onstyle="success" data-offstyle="primary" checked>
|
||||||
</div>
|
</div>
|
||||||
`).on('keydown', this.navigateUpAndDown.bind(this));
|
<div id='query_list' class='query-history' tabindex='0'></div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
self.$entriesEl = self.$el.find('#query_list');
|
||||||
|
self.$entriesEl.on('keydown', this.navigateUpAndDown.bind(this));
|
||||||
|
|
||||||
|
self.is_pgadmin_queries_shown = true;
|
||||||
|
|
||||||
|
self.$el.find('#generated-queries-toggle').bootstrapToggle().change(
|
||||||
|
function() {
|
||||||
|
self.toggleGeneratedQueries();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
self.parentNode.empty().append(self.$el);
|
self.parentNode.empty().append(self.$el);
|
||||||
}
|
}
|
||||||
|
|||||||
35
web/pgadmin/static/js/sqleditor/history/query_sources.js
Normal file
35
web/pgadmin/static/js/sqleditor/history/query_sources.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// pgAdmin 4 - PostgreSQL Tools
|
||||||
|
//
|
||||||
|
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
|
||||||
|
// This software is released under the PostgreSQL Licence
|
||||||
|
//
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/* This file contains the source of the queries in the history and their
|
||||||
|
respective icons css classes */
|
||||||
|
|
||||||
|
export const QuerySources = {
|
||||||
|
EXECUTE: {
|
||||||
|
ICON_CSS_CLASS: 'fa fa-bolt',
|
||||||
|
},
|
||||||
|
EXPLAIN: {
|
||||||
|
ICON_CSS_CLASS: 'fa fa-hand-pointer-o',
|
||||||
|
},
|
||||||
|
EXPLAIN_ANALYZE: {
|
||||||
|
ICON_CSS_CLASS: 'fa fa-list-alt',
|
||||||
|
},
|
||||||
|
COMMIT: {
|
||||||
|
ICON_CSS_CLASS: 'icon-commit',
|
||||||
|
},
|
||||||
|
ROLLBACK: {
|
||||||
|
ICON_CSS_CLASS: 'icon-rollback',
|
||||||
|
},
|
||||||
|
SAVE_DATA: {
|
||||||
|
ICON_CSS_CLASS: 'icon-save-data-changes',
|
||||||
|
},
|
||||||
|
VIEW_DATA: {
|
||||||
|
ICON_CSS_CLASS: 'icon-view-data',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -696,7 +696,7 @@ def generate_client_primary_key_name(columns_info):
|
|||||||
@login_required
|
@login_required
|
||||||
def save(trans_id):
|
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:
|
Args:
|
||||||
trans_id: unique transaction id
|
trans_id: unique transaction id
|
||||||
@@ -746,7 +746,7 @@ def save(trans_id):
|
|||||||
return make_json_response(
|
return make_json_response(
|
||||||
data={'status': status, 'result': u"{}".format(msg)}
|
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,
|
changed_data,
|
||||||
session_obj['columns_info'],
|
session_obj['columns_info'],
|
||||||
session_obj['client_primary_key'],
|
session_obj['client_primary_key'],
|
||||||
@@ -754,7 +754,7 @@ def save(trans_id):
|
|||||||
else:
|
else:
|
||||||
status = False
|
status = False
|
||||||
res = error_msg
|
res = error_msg
|
||||||
query_res = None
|
query_results = None
|
||||||
_rowid = None
|
_rowid = None
|
||||||
|
|
||||||
transaction_status = conn.transaction_status()
|
transaction_status = conn.transaction_status()
|
||||||
@@ -763,7 +763,7 @@ def save(trans_id):
|
|||||||
data={
|
data={
|
||||||
'status': status,
|
'status': status,
|
||||||
'result': res,
|
'result': res,
|
||||||
'query_result': query_res,
|
'query_results': query_results,
|
||||||
'_rowid': _rowid,
|
'_rowid': _rowid,
|
||||||
'transaction_status': transaction_status
|
'transaction_status': transaction_status
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ input.editor-checkbox:focus {
|
|||||||
background-image: url('../img/disconnect.svg');
|
background-image: url('../img/disconnect.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-commit, .icon-rollback, .icon-save-data-changes {
|
.icon-commit, .icon-rollback, .icon-save-data-changes, .icon-view-data {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
@@ -315,6 +315,10 @@ input.editor-checkbox:focus {
|
|||||||
background-image: url('../img/save_data_changes.svg') !important;
|
background-image: url('../img/save_data_changes.svg') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-view-data {
|
||||||
|
background-image: url('../img/view_data.svg') !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ajs-body .warn-header {
|
.ajs-body .warn-header {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|||||||
9
web/pgadmin/tools/sqleditor/static/img/view_data.svg
Normal file
9
web/pgadmin/tools/sqleditor/static/img/view_data.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.3 KiB |
@@ -27,6 +27,7 @@ define('tools.querytool', [
|
|||||||
'sources/sqleditor/geometry_viewer',
|
'sources/sqleditor/geometry_viewer',
|
||||||
'sources/sqleditor/history/history_collection.js',
|
'sources/sqleditor/history/history_collection.js',
|
||||||
'sources/sqleditor/history/query_history',
|
'sources/sqleditor/history/query_history',
|
||||||
|
'sources/sqleditor/history/query_sources',
|
||||||
'sources/keyboard_shortcuts',
|
'sources/keyboard_shortcuts',
|
||||||
'sources/sqleditor/query_tool_actions',
|
'sources/sqleditor/query_tool_actions',
|
||||||
'sources/sqleditor/query_tool_notifications',
|
'sources/sqleditor/query_tool_notifications',
|
||||||
@@ -49,7 +50,7 @@ define('tools.querytool', [
|
|||||||
babelPollyfill, gettext, url_for, $, jqueryui, jqueryui_position, _, S, alertify, pgAdmin, Backbone, codemirror,
|
babelPollyfill, gettext, url_for, $, jqueryui, jqueryui_position, _, S, alertify, pgAdmin, Backbone, codemirror,
|
||||||
pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent,
|
pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent,
|
||||||
XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler,
|
XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler,
|
||||||
GeometryViewer, historyColl, queryHist,
|
GeometryViewer, historyColl, queryHist, querySources,
|
||||||
keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid,
|
keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid,
|
||||||
modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, csrfToken, panelTitleFunc) {
|
modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, csrfToken, panelTitleFunc) {
|
||||||
/* Return back, this has been called more than once */
|
/* Return back, this has been called more than once */
|
||||||
@@ -63,7 +64,8 @@ define('tools.querytool', [
|
|||||||
CodeMirror = codemirror.default,
|
CodeMirror = codemirror.default,
|
||||||
Slick = window.Slick,
|
Slick = window.Slick,
|
||||||
HistoryCollection = historyColl.default,
|
HistoryCollection = historyColl.default,
|
||||||
QueryHistory = queryHist.default;
|
QueryHistory = queryHist.default,
|
||||||
|
QuerySources = querySources.QuerySources;
|
||||||
|
|
||||||
csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token);
|
csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token);
|
||||||
|
|
||||||
@@ -1361,26 +1363,26 @@ define('tools.querytool', [
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make ajax call to get history data except view/edit data
|
// Make ajax call to get history data
|
||||||
if(self.handler.is_query_tool) {
|
$.ajax({
|
||||||
$.ajax({
|
url: url_for('sqleditor.get_query_history', {
|
||||||
url: url_for('sqleditor.get_query_history', {
|
'trans_id': self.handler.transId,
|
||||||
'trans_id': self.handler.transId,
|
}),
|
||||||
}),
|
method: 'GET',
|
||||||
method: 'GET',
|
contentType: 'application/json',
|
||||||
contentType: 'application/json',
|
})
|
||||||
})
|
.done(function(res) {
|
||||||
.done(function(res) {
|
res.data.result.map((entry) => {
|
||||||
res.data.result.map((entry) => {
|
let newEntry = JSON.parse(entry);
|
||||||
let newEntry = JSON.parse(entry);
|
newEntry.start_time = new Date(newEntry.start_time);
|
||||||
newEntry.start_time = new Date(newEntry.start_time);
|
self.history_collection.add(newEntry);
|
||||||
self.history_collection.add(newEntry);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.fail(function() {
|
|
||||||
/* history fetch fail should not affect query tool */
|
|
||||||
});
|
});
|
||||||
} else {
|
})
|
||||||
|
.fail(function() {
|
||||||
|
/* history fetch fail should not affect query tool */
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!self.handler.is_query_tool) {
|
||||||
self.historyComponent.setEditorPref({'copy_to_editor':false});
|
self.historyComponent.setEditorPref({'copy_to_editor':false});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1611,11 +1613,15 @@ define('tools.querytool', [
|
|||||||
|
|
||||||
// Callback function for Save Data Changes button click.
|
// Callback function for Save Data Changes button click.
|
||||||
on_save_data: function() {
|
on_save_data: function() {
|
||||||
|
this.handler.history_query_source = QuerySources.SAVE_DATA;
|
||||||
|
|
||||||
queryToolActions.saveDataChanges(this.handler);
|
queryToolActions.saveDataChanges(this.handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Callback function for the flash button click.
|
// Callback function for the flash button click.
|
||||||
on_flash: function() {
|
on_flash: function() {
|
||||||
|
this.handler.history_query_source = QuerySources.EXECUTE;
|
||||||
|
|
||||||
queryToolActions.executeQuery(this.handler);
|
queryToolActions.executeQuery(this.handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1771,6 +1777,7 @@ define('tools.querytool', [
|
|||||||
this._stopEventPropogation(event);
|
this._stopEventPropogation(event);
|
||||||
this._closeDropDown(event);
|
this._closeDropDown(event);
|
||||||
|
|
||||||
|
this.handler.history_query_source = QuerySources.EXPLAIN;
|
||||||
queryToolActions.explain(this.handler);
|
queryToolActions.explain(this.handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1779,6 +1786,7 @@ define('tools.querytool', [
|
|||||||
this._stopEventPropogation(event);
|
this._stopEventPropogation(event);
|
||||||
this._closeDropDown(event);
|
this._closeDropDown(event);
|
||||||
|
|
||||||
|
this.handler.history_query_source = QuerySources.EXPLAIN_ANALYZE;
|
||||||
queryToolActions.explainAnalyze(this.handler);
|
queryToolActions.explainAnalyze(this.handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1903,12 +1911,16 @@ define('tools.querytool', [
|
|||||||
// Callback function for the commit button click.
|
// Callback function for the commit button click.
|
||||||
on_commit_transaction: function() {
|
on_commit_transaction: function() {
|
||||||
this.handler.close_on_idle_transaction = false;
|
this.handler.close_on_idle_transaction = false;
|
||||||
|
this.handler.history_query_source = QuerySources.COMMIT;
|
||||||
|
|
||||||
queryToolActions.executeCommit(this.handler);
|
queryToolActions.executeCommit(this.handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Callback function for the rollback button click.
|
// Callback function for the rollback button click.
|
||||||
on_rollback_transaction: function() {
|
on_rollback_transaction: function() {
|
||||||
this.handler.close_on_idle_transaction = false;
|
this.handler.close_on_idle_transaction = false;
|
||||||
|
this.handler.history_query_source = QuerySources.ROLLBACK;
|
||||||
|
|
||||||
queryToolActions.executeRollback(this.handler);
|
queryToolActions.executeRollback(this.handler);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -2710,36 +2722,48 @@ define('tools.querytool', [
|
|||||||
new Date());
|
new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
let hist_entry = {
|
if(_.isUndefined(self.history_query_source)) {
|
||||||
|
self.history_query_source = QuerySources.VIEW_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
let history_entry = {
|
||||||
'status': status,
|
'status': status,
|
||||||
'start_time': self.query_start_time,
|
'start_time': self.query_start_time,
|
||||||
'query': self.query,
|
'query': self.query,
|
||||||
'row_affected': self.rows_affected,
|
'row_affected': self.rows_affected,
|
||||||
'total_time': self.total_time,
|
'total_time': self.total_time,
|
||||||
'message': msg,
|
'message': msg,
|
||||||
|
'query_source': self.history_query_source,
|
||||||
|
'is_pgadmin_query': !self.is_query_tool,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Make ajax call to save the history data
|
if(!self.is_query_tool) {
|
||||||
* Do not bother query tool if failed to save
|
var info_msg = gettext('This query was generated by pgAdmin as part of a "View/Edit Data" operation');
|
||||||
* Not applicable for view/edit data
|
history_entry.info = info_msg;
|
||||||
*/
|
|
||||||
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
|
/* This function is used to check whether cell
|
||||||
* is editable or not depending on primary keys
|
* is editable or not depending on primary keys
|
||||||
* and staged_rows flag
|
* and staged_rows flag
|
||||||
@@ -2900,7 +2924,7 @@ define('tools.querytool', [
|
|||||||
var req_data = self.data_store, view = self.gridView;
|
var req_data = self.data_store, view = self.gridView;
|
||||||
req_data.columns = view ? view.handler.columns : self.columns;
|
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
|
// Make ajax call to save the data
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -2949,7 +2973,7 @@ define('tools.querytool', [
|
|||||||
if(is_added) {
|
if(is_added) {
|
||||||
// Update the rows in a grid after addition
|
// Update the rows in a grid after addition
|
||||||
dataView.beginUpdate();
|
dataView.beginUpdate();
|
||||||
_.each(res.data.query_result, function(r) {
|
_.each(res.data.query_results, function(r) {
|
||||||
if (!_.isNull(r.row_added)) {
|
if (!_.isNull(r.row_added)) {
|
||||||
// Fetch temp_id returned by server after addition
|
// Fetch temp_id returned by server after addition
|
||||||
var row_id = Object.keys(r.row_added)[0];
|
var row_id = Object.keys(r.row_added)[0];
|
||||||
@@ -3043,6 +3067,23 @@ define('tools.querytool', [
|
|||||||
grid.gotoCell(_row_index, 1);
|
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,
|
||||||
|
'query_source': QuerySources.SAVE_DATA,
|
||||||
|
'is_pgadmin_query': true,
|
||||||
|
'info': query_history_info_msg,
|
||||||
|
};
|
||||||
|
self.add_to_history(history_entry);
|
||||||
|
});
|
||||||
|
|
||||||
self.trigger('pgadmin-sqleditor:loading-icon:hide');
|
self.trigger('pgadmin-sqleditor:loading-icon:hide');
|
||||||
|
|
||||||
grid.invalidate();
|
grid.invalidate();
|
||||||
@@ -3635,7 +3676,6 @@ define('tools.querytool', [
|
|||||||
mode_disabled = true;
|
mode_disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#btn-clear-dropdown').prop('disabled', mode_disabled);
|
|
||||||
$('#btn-explain').prop('disabled', mode_disabled);
|
$('#btn-explain').prop('disabled', mode_disabled);
|
||||||
$('#btn-explain-analyze').prop('disabled', mode_disabled);
|
$('#btn-explain-analyze').prop('disabled', mode_disabled);
|
||||||
$('#btn-explain-options-dropdown').prop('disabled', mode_disabled);
|
$('#btn-explain-options-dropdown').prop('disabled', mode_disabled);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.query-history {
|
.query-history {
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
border-bottom: $panel-border;
|
border-bottom: $panel-border;
|
||||||
background-color: $color-bg-theme;
|
background-color: $color-bg-theme;
|
||||||
@@ -30,6 +30,11 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
user-select: initial;
|
user-select: initial;
|
||||||
|
|
||||||
|
.query-history-icon {
|
||||||
|
width: 18px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +116,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 {
|
.metadata-block {
|
||||||
flex: 0.4;
|
flex: 0.4;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
@@ -219,4 +235,15 @@
|
|||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggle-and-history-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.query-history-toggle {
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ def save_changed_data(changed_data, columns_info, conn, command_obj,
|
|||||||
"""
|
"""
|
||||||
status = False
|
status = False
|
||||||
res = None
|
res = None
|
||||||
query_res = dict()
|
query_results = []
|
||||||
count = 0
|
|
||||||
list_of_rowid = []
|
|
||||||
operations = ('added', 'updated', 'deleted')
|
operations = ('added', 'updated', 'deleted')
|
||||||
list_of_sql = {}
|
list_of_sql = {}
|
||||||
_rowid = None
|
_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()
|
for col_name, col_info in columns_info.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
if conn.connected():
|
is_savepoint = False
|
||||||
is_savepoint = False
|
# Start the transaction if the session is idle
|
||||||
# Start the transaction if the session is idle
|
if conn.transaction_status() == TX_STATUS_IDLE:
|
||||||
if conn.transaction_status() == TX_STATUS_IDLE:
|
sql = 'BEGIN;'
|
||||||
conn.execute_void('BEGIN;')
|
else:
|
||||||
else:
|
sql = 'SAVEPOINT save_data;'
|
||||||
conn.execute_void('SAVEPOINT save_data;')
|
is_savepoint = True
|
||||||
is_savepoint = True
|
|
||||||
|
|
||||||
# Iterate total number of records to be updated/inserted
|
status, res = execute_void_wrapper(conn, sql, query_results)
|
||||||
for of_type in changed_data:
|
if not status:
|
||||||
# No need to go further if its not add/update/delete operation
|
return status, res, query_results, None
|
||||||
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 = {}
|
# Iterate total number of records to be updated/inserted
|
||||||
column_data = {}
|
for of_type in changed_data:
|
||||||
for each_col in columns_info:
|
# No need to go further if its not add/update/delete operation
|
||||||
if (
|
if of_type not in operations:
|
||||||
columns_info[each_col]['not_null'] and
|
continue
|
||||||
not columns_info[each_col]['has_default_val']
|
# if no data to be save then continue
|
||||||
):
|
if len(changed_data[of_type]) < 1:
|
||||||
column_data[each_col] = None
|
continue
|
||||||
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
|
column_type = {}
|
||||||
if of_type == 'added':
|
column_data = {}
|
||||||
# Python dict does not honour the inserted item order
|
for each_col in columns_info:
|
||||||
# So to insert data in the order, we need to make ordered
|
if (
|
||||||
# list of added index We don't need this mechanism in
|
columns_info[each_col]['not_null'] and
|
||||||
# updated/deleted rows as it does not matter in
|
not columns_info[each_col]['has_default_val']
|
||||||
# those operations
|
):
|
||||||
added_index = OrderedDict(
|
column_data[each_col] = None
|
||||||
sorted(
|
column_type[each_col] = \
|
||||||
changed_data['added_index'].items(),
|
columns_info[each_col]['type_name']
|
||||||
key=lambda x: int(x[0])
|
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
|
# When new rows are added, only changed columns data is
|
||||||
# sent from client side. But if column is not_null and has
|
# sent from client side. But if column is not_null and has
|
||||||
# no_default_value, set column to blank, instead
|
# no_default_value, set column to blank, instead
|
||||||
# of not null which is set by default.
|
# of not null which is set by default.
|
||||||
column_data = {}
|
column_data = {}
|
||||||
pk_names, primary_keys = command_obj.get_primary_keys()
|
pk_names, primary_keys = command_obj.get_primary_keys()
|
||||||
has_oids = 'oid' in column_type
|
has_oids = 'oid' in column_type
|
||||||
|
|
||||||
for each_row in added_index:
|
for each_row in added_index:
|
||||||
# Get the row index to match with the added rows
|
# Get the row index to match with the added rows
|
||||||
# dict key
|
# dict key
|
||||||
tmp_row_index = added_index[each_row]
|
tmp_row_index = added_index[each_row]
|
||||||
data = changed_data[of_type][tmp_row_index]['data']
|
data = changed_data[of_type][tmp_row_index]['data']
|
||||||
# Remove our unique tracking key
|
# Remove our unique tracking key
|
||||||
data.pop(client_primary_key, None)
|
data.pop(client_primary_key, None)
|
||||||
data.pop('is_row_copied', None)
|
data.pop('is_row_copied', None)
|
||||||
list_of_rowid.append(data.get(client_primary_key))
|
|
||||||
|
|
||||||
# Update columns value with columns having
|
# Update columns value with columns having
|
||||||
# not_null=False and has no default value
|
# not_null=False and has no default value
|
||||||
column_data.update(data)
|
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
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
# 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]
|
|
||||||
|
|
||||||
sql = render_template(
|
sql = render_template(
|
||||||
"/".join([command_obj.sql_path, 'delete.sql']),
|
"/".join([command_obj.sql_path, 'insert.sql']),
|
||||||
data=rows_to_delete,
|
data_to_be_saved=column_data,
|
||||||
primary_key_labels=keys,
|
pgadmin_alias=pgadmin_alias,
|
||||||
no_of_keys=no_of_keys,
|
primary_keys=None,
|
||||||
object_name=command_obj.object_name,
|
object_name=command_obj.object_name,
|
||||||
nsp_name=command_obj.nsp_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': {}})
|
|
||||||
|
|
||||||
for opr, sqls in list_of_sql.items():
|
select_sql = render_template(
|
||||||
for item in sqls:
|
"/".join([command_obj.sql_path, 'select.sql']),
|
||||||
if item['sql']:
|
object_name=command_obj.object_name,
|
||||||
item['data'] = {
|
nsp_name=command_obj.nsp_name,
|
||||||
pgadmin_alias[k] if k in pgadmin_alias else k: v
|
primary_keys=primary_keys,
|
||||||
for k, v in item['data'].items()
|
has_oids=has_oids
|
||||||
}
|
)
|
||||||
|
|
||||||
row_added = None
|
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 = {}
|
||||||
|
|
||||||
def failure_handle(res):
|
# For updated rows
|
||||||
if is_savepoint:
|
elif of_type == 'updated':
|
||||||
conn.execute_void('ROLLBACK TO SAVEPOINT '
|
list_of_sql[of_type] = []
|
||||||
'save_data;')
|
for each_row in changed_data[of_type]:
|
||||||
msg = 'Query ROLLBACK, but the current ' \
|
data = changed_data[of_type][each_row]['data']
|
||||||
'transaction is still ongoing.'
|
pk_escaped = {
|
||||||
else:
|
pk: pk_val.replace('%', '%%') if hasattr(
|
||||||
conn.execute_void('ROLLBACK;')
|
pk_val, 'replace') else pk_val
|
||||||
msg = 'Transaction ROLLBACK'
|
for pk, pk_val in
|
||||||
# If we roll backed every thing then update the
|
changed_data[of_type][each_row]['primary_keys'].items()
|
||||||
# message for each sql query.
|
}
|
||||||
for val in query_res:
|
sql = render_template(
|
||||||
if query_res[val]['status']:
|
"/".join([command_obj.sql_path, 'update.sql']),
|
||||||
query_res[val]['result'] = msg
|
data_to_be_saved=data,
|
||||||
|
pgadmin_alias=pgadmin_alias,
|
||||||
# If list is empty set rowid to 1
|
primary_keys=pk_escaped,
|
||||||
try:
|
object_name=command_obj.object_name,
|
||||||
if list_of_rowid:
|
nsp_name=command_obj.nsp_name,
|
||||||
_rowid = list_of_rowid[count]
|
data_type=column_type
|
||||||
else:
|
)
|
||||||
_rowid = 1
|
list_of_sql[of_type].append({'sql': sql,
|
||||||
except Exception:
|
'data': data,
|
||||||
_rowid = 0
|
'row_id':
|
||||||
|
data.get(client_primary_key)})
|
||||||
return status, res, query_res, _rowid
|
|
||||||
|
|
||||||
|
# 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:
|
try:
|
||||||
# Fetch oids/primary keys
|
row[changed_data['columns']
|
||||||
if 'select_sql' in item and item['select_sql']:
|
[int(k)]['name']] = v
|
||||||
status, res = conn.execute_dict(
|
except ValueError:
|
||||||
item['sql'], item['data'])
|
continue
|
||||||
else:
|
del row[k]
|
||||||
status, res = conn.execute_void(
|
|
||||||
item['sql'], item['data'])
|
sql = render_template(
|
||||||
except Exception as _:
|
"/".join([command_obj.sql_path, 'delete.sql']),
|
||||||
failure_handle(res)
|
data=rows_to_delete,
|
||||||
raise
|
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:
|
if not status:
|
||||||
return failure_handle(res)
|
return failure_handle(sel_res, item.get('row_id', 0))
|
||||||
|
|
||||||
# Select added row from the table
|
if 'rows' in sel_res and len(sel_res['rows']) > 0:
|
||||||
if 'select_sql' in item:
|
row_added = {
|
||||||
status, sel_res = conn.execute_dict(
|
item['client_row']: sel_res['rows'][0]}
|
||||||
item['select_sql'], res['rows'][0])
|
|
||||||
|
|
||||||
if not status:
|
rows_affected = conn.rows_affected()
|
||||||
if is_savepoint:
|
mogrified_sql = conn.mogrify(item['sql'], item['data'])
|
||||||
conn.execute_void('ROLLBACK TO SAVEPOINT'
|
mogrified_sql = mogrified_sql if mogrified_sql is not None \
|
||||||
' save_data;')
|
else item['sql']
|
||||||
msg = 'Query ROLLBACK, the current' \
|
# store the result of each query in dictionary
|
||||||
' transaction is still ongoing.'
|
query_results.append({
|
||||||
else:
|
'status': status,
|
||||||
conn.execute_void('ROLLBACK;')
|
'result': None if row_added else res,
|
||||||
msg = 'Transaction ROLLBACK'
|
'sql': mogrified_sql,
|
||||||
# If we roll backed every thing then update
|
'rows_affected': rows_affected,
|
||||||
# the message for each sql query.
|
'row_added': row_added
|
||||||
for val in query_res:
|
})
|
||||||
if query_res[val]['status']:
|
|
||||||
query_res[val]['result'] = msg
|
|
||||||
|
|
||||||
# If list is empty set rowid to 1
|
# Commit the transaction if no error is found & autocommit is activated
|
||||||
try:
|
if auto_commit:
|
||||||
if list_of_rowid:
|
sql = 'COMMIT;'
|
||||||
_rowid = list_of_rowid[count]
|
status, res = execute_void_wrapper(conn, sql, query_results)
|
||||||
else:
|
if not status:
|
||||||
_rowid = 1
|
return status, res, query_results, None
|
||||||
except Exception:
|
|
||||||
_rowid = 0
|
|
||||||
|
|
||||||
return status, sel_res, query_res, _rowid
|
return status, res, query_results, _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()
|
def execute_void_wrapper(conn, sql, query_results):
|
||||||
# store the result of each query in dictionary
|
"""
|
||||||
query_res[count] = {
|
Executes a sql query with no return and adds it to query_results
|
||||||
'status': status,
|
:param sql: Sql query
|
||||||
'result': None if row_added else res,
|
:param query_results: A list of query results in the save operation
|
||||||
'sql': item['sql'], 'rows_affected': rows_affected,
|
:return: status, result
|
||||||
'row_added': row_added
|
"""
|
||||||
}
|
status, res = conn.execute_void(sql)
|
||||||
|
if status:
|
||||||
count += 1
|
query_results.append({
|
||||||
|
'status': status,
|
||||||
# Commit the transaction if no error is found & autocommit is activated
|
'result': res,
|
||||||
if auto_commit:
|
'sql': sql, 'rows_affected': 0,
|
||||||
conn.execute_void('COMMIT;')
|
'row_added': None
|
||||||
|
})
|
||||||
return status, res, query_res, _rowid
|
return status, res
|
||||||
|
|||||||
@@ -116,8 +116,9 @@ class TestSaveChangedData(BaseTestGenerator):
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
save_status=False,
|
save_status=False,
|
||||||
check_sql=None,
|
check_sql="SELECT * FROM %s "
|
||||||
check_result=None
|
"WHERE pk_col = 1 AND normal_col = 'four'",
|
||||||
|
check_result='SELECT 0'
|
||||||
)),
|
)),
|
||||||
('When updating a row in a valid way', dict(
|
('When updating a row in a valid way', dict(
|
||||||
save_payload={
|
save_payload={
|
||||||
@@ -171,9 +172,9 @@ class TestSaveChangedData(BaseTestGenerator):
|
|||||||
"updated": {
|
"updated": {
|
||||||
"1":
|
"1":
|
||||||
{"err": False,
|
{"err": False,
|
||||||
"data": {"pk_col": "2"},
|
"data": {"pk_col": "1"},
|
||||||
"primary_keys":
|
"primary_keys":
|
||||||
{"pk_col": 1}
|
{"pk_col": 2}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"added": {},
|
"added": {},
|
||||||
@@ -210,8 +211,9 @@ class TestSaveChangedData(BaseTestGenerator):
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
save_status=False,
|
save_status=False,
|
||||||
check_sql=None,
|
check_sql="SELECT * FROM %s "
|
||||||
check_result=None
|
"WHERE pk_col = 1 AND normal_col = 'two'",
|
||||||
|
check_result='SELECT 0'
|
||||||
)),
|
)),
|
||||||
('When deleting a row', dict(
|
('When deleting a row', dict(
|
||||||
save_payload={
|
save_payload={
|
||||||
@@ -283,20 +285,19 @@ class TestSaveChangedData(BaseTestGenerator):
|
|||||||
save_status = response_data['data']['status']
|
save_status = response_data['data']['status']
|
||||||
self.assertEquals(save_status, self.save_status)
|
self.assertEquals(save_status, self.save_status)
|
||||||
|
|
||||||
if self.check_sql:
|
# Execute check sql
|
||||||
# Execute check sql
|
# Add test table name to the query
|
||||||
# Add test table name to the query
|
check_sql = self.check_sql % self.test_table_name
|
||||||
check_sql = self.check_sql % self.test_table_name
|
is_success, response_data = \
|
||||||
is_success, response_data = \
|
execute_query(tester=self.tester,
|
||||||
execute_query(tester=self.tester,
|
query=check_sql,
|
||||||
query=check_sql,
|
start_query_tool_url=self.start_query_tool_url,
|
||||||
start_query_tool_url=self.start_query_tool_url,
|
poll_url=self.poll_url)
|
||||||
poll_url=self.poll_url)
|
self.assertEquals(is_success, True)
|
||||||
self.assertEquals(is_success, True)
|
|
||||||
|
|
||||||
# Check table for updates
|
# Check table for updates
|
||||||
result = response_data['data']['result']
|
result = response_data['data']['result']
|
||||||
self.assertEquals(result, self.check_result)
|
self.assertEquals(result, self.check_result)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
# Disconnect the database
|
# Disconnect the database
|
||||||
|
|||||||
@@ -1917,3 +1917,17 @@ Failed to reset the connection to the server due to following error:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return enc_password
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user