diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/alter_fdw_change_opt2.sql b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/alter_fdw_change_opt2.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/alter_fdw_change_opt2.sql rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/alter_fdw_change_opt2.sql diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/alter_fdw_comment.sql b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/alter_fdw_comment.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/alter_fdw_comment.sql rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/alter_fdw_comment.sql diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/alter_fdw_opt1.sql b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/alter_fdw_opt1.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/alter_fdw_opt1.sql rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/alter_fdw_opt1.sql diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/alter_fdw_opt2.sql b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/alter_fdw_opt2.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/alter_fdw_opt2.sql rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/alter_fdw_opt2.sql diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/alter_fdw_rename.sql b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/alter_fdw_rename.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/alter_fdw_rename.sql rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/alter_fdw_rename.sql diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/alter_fdw_validator.sql b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/alter_fdw_validator.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/alter_fdw_validator.sql rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/alter_fdw_validator.sql diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/create_fdw.sql b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/create_fdw.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/create_fdw.sql rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/create_fdw.sql diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/test.json b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/test.json similarity index 95% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/test.json rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/test.json index 21501b4a2..f24e28771 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/9.3_plus/test.json +++ b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/9.3_plus/test.json @@ -98,7 +98,10 @@ },{ "type": "delete", "name": "Drop FDW", - "endpoint": "NODE-foreign_data_wrapper.delete_id" + "endpoint": "NODE-foreign_data_wrapper.delete_id", + "data": { + "name": "Fdw2_$%{}[]()&*^!@\"'`\\/#" + } } ] } diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/alter_fdw_change_opt2.sql b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/alter_fdw_change_opt2.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/alter_fdw_change_opt2.sql rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/alter_fdw_change_opt2.sql diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/alter_fdw_comment.sql b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/alter_fdw_comment.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/alter_fdw_comment.sql rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/alter_fdw_comment.sql diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/alter_fdw_opt1.sql b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/alter_fdw_opt1.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/alter_fdw_opt1.sql rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/alter_fdw_opt1.sql diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/alter_fdw_opt2.sql b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/alter_fdw_opt2.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/alter_fdw_opt2.sql rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/alter_fdw_opt2.sql diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/alter_fdw_rename.sql b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/alter_fdw_rename.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/alter_fdw_rename.sql rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/alter_fdw_rename.sql diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/alter_fdw_validator.sql b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/alter_fdw_validator.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/alter_fdw_validator.sql rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/alter_fdw_validator.sql diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/create_fdw.sql b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/create_fdw.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/create_fdw.sql rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/create_fdw.sql diff --git a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/test.json b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/test.json similarity index 95% rename from web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/test.json rename to web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/test.json index 21501b4a2..f24e28771 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/default/test.json +++ b/web/pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/tests/pg/default/test.json @@ -98,7 +98,10 @@ },{ "type": "delete", "name": "Drop FDW", - "endpoint": "NODE-foreign_data_wrapper.delete_id" + "endpoint": "NODE-foreign_data_wrapper.delete_id", + "data": { + "name": "Fdw2_$%{}[]()&*^!@\"'`\\/#" + } } ] } diff --git a/web/pgadmin/browser/server_groups/servers/resource_groups/tests/default/alter_resource_group_name.sql b/web/pgadmin/browser/server_groups/servers/resource_groups/tests/ppas/9.4_plus/alter_resource_group_name.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/resource_groups/tests/default/alter_resource_group_name.sql rename to web/pgadmin/browser/server_groups/servers/resource_groups/tests/ppas/9.4_plus/alter_resource_group_name.sql diff --git a/web/pgadmin/browser/server_groups/servers/resource_groups/tests/default/alter_resource_group_options.sql b/web/pgadmin/browser/server_groups/servers/resource_groups/tests/ppas/9.4_plus/alter_resource_group_options.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/resource_groups/tests/default/alter_resource_group_options.sql rename to web/pgadmin/browser/server_groups/servers/resource_groups/tests/ppas/9.4_plus/alter_resource_group_options.sql diff --git a/web/pgadmin/browser/server_groups/servers/resource_groups/tests/default/create_resource_group.sql b/web/pgadmin/browser/server_groups/servers/resource_groups/tests/ppas/9.4_plus/create_resource_group.sql similarity index 100% rename from web/pgadmin/browser/server_groups/servers/resource_groups/tests/default/create_resource_group.sql rename to web/pgadmin/browser/server_groups/servers/resource_groups/tests/ppas/9.4_plus/create_resource_group.sql diff --git a/web/pgadmin/browser/server_groups/servers/resource_groups/tests/default/test.json b/web/pgadmin/browser/server_groups/servers/resource_groups/tests/ppas/9.4_plus/test.json similarity index 93% rename from web/pgadmin/browser/server_groups/servers/resource_groups/tests/default/test.json rename to web/pgadmin/browser/server_groups/servers/resource_groups/tests/ppas/9.4_plus/test.json index 030653612..312eb3df4 100644 --- a/web/pgadmin/browser/server_groups/servers/resource_groups/tests/default/test.json +++ b/web/pgadmin/browser/server_groups/servers/resource_groups/tests/ppas/9.4_plus/test.json @@ -1,9 +1,4 @@ { - "prerequisite": { - "minVer": 90400, - "maxVer": null, - "type": "ppas" - }, "scenarios": [ { "type": "create", diff --git a/web/pgadmin/tools/sqleditor/static/img/save_data_changes.svg b/web/pgadmin/tools/sqleditor/static/img/save_data_changes.svg new file mode 100644 index 000000000..09ead9286 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/static/img/save_data_changes.svg @@ -0,0 +1,12 @@ + + +save_data_changes + + +Layer 1 + + + \ No newline at end of file diff --git a/web/pgadmin/tools/sqleditor/tests/execute_query_utils.py b/web/pgadmin/tools/sqleditor/tests/execute_query_utils.py new file mode 100644 index 000000000..af10564dc --- /dev/null +++ b/web/pgadmin/tools/sqleditor/tests/execute_query_utils.py @@ -0,0 +1,41 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2019, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import json + +# Utility functions used by tests + + +# Executes a query and polls for the results, then returns them +def execute_query(tester, query, start_query_tool_url, poll_url): + # Start query tool and execute sql + response = tester.post(start_query_tool_url, + data=json.dumps({"sql": query}), + content_type='html/json') + + if response.status_code != 200: + return False, None + + # Poll for results + return poll_for_query_results(tester=tester, poll_url=poll_url) + + +# Polls for the result of an executed query +def poll_for_query_results(tester, poll_url): + # Poll for results until they are successful + while True: + response = tester.get(poll_url) + if response.status_code != 200: + return False, None + response_data = json.loads(response.data.decode('utf-8')) + status = response_data['data']['status'] + if status == 'Success': + return True, response_data + elif status == 'NotConnected' or status == 'Cancel': + return False, None diff --git a/web/pgadmin/tools/sqleditor/tests/test_is_query_resultset_updatable.py b/web/pgadmin/tools/sqleditor/tests/test_is_query_resultset_updatable.py new file mode 100644 index 000000000..d7e75c780 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/tests/test_is_query_resultset_updatable.py @@ -0,0 +1,125 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2019, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import json + +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from .execute_query_utils import execute_query + + +class TestQueryUpdatableResultset(BaseTestGenerator): + """ This class will test the detection of whether the query + result-set is updatable. """ + scenarios = [ + ('When selecting all columns of the table', dict( + sql='SELECT * FROM test_for_updatable_resultset;', + primary_keys={ + 'pk_col1': 'int4', + 'pk_col2': 'int4' + } + )), + ('When selecting all primary keys of the table', dict( + sql='SELECT pk_col1, pk_col2 FROM test_for_updatable_resultset;', + primary_keys={ + 'pk_col1': 'int4', + 'pk_col2': 'int4' + } + )), + ('When selecting some of the primary keys of the table', dict( + sql='SELECT pk_col2 FROM test_for_updatable_resultset;', + primary_keys=None + )), + ('When selecting none of the primary keys of the table', dict( + sql='SELECT normal_col1 FROM test_for_updatable_resultset;', + primary_keys=None + )), + ('When renaming a primary key', dict( + sql='SELECT pk_col1 as some_col, ' + 'pk_col2 FROM test_for_updatable_resultset;', + primary_keys=None + )), + ('When renaming a column to a primary key name', dict( + sql='SELECT pk_col1, pk_col2, normal_col1 as pk_col1 ' + 'FROM test_for_updatable_resultset;', + primary_keys=None + )) + ] + + def setUp(self): + self._initialize_database_connection() + self._initialize_query_tool() + self._initialize_urls() + self._create_test_table() + + def runTest(self): + is_success, response_data = \ + execute_query(tester=self.tester, + query=self.sql, + poll_url=self.poll_url, + start_query_tool_url=self.start_query_tool_url) + self.assertEquals(is_success, True) + + # Check primary keys + primary_keys = response_data['data']['primary_keys'] + self.assertEquals(primary_keys, self.primary_keys) + + def tearDown(self): + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) + + def _initialize_database_connection(self): + database_info = parent_node_dict["database"][-1] + self.server_id = database_info["server_id"] + + self.db_id = database_info["db_id"] + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if not db_con["info"] == "Database connected.": + raise Exception("Could not connect to the database.") + + def _initialize_query_tool(self): + url = '/datagrid/initialize/query_tool/{0}/{1}/{2}'.format( + utils.SERVER_GROUP, self.server_id, self.db_id) + response = self.tester.post(url) + self.assertEquals(response.status_code, 200) + + response_data = json.loads(response.data.decode('utf-8')) + self.trans_id = response_data['data']['gridTransId'] + + def _initialize_urls(self): + self.start_query_tool_url = \ + '/sqleditor/query_tool/start/{0}'.format(self.trans_id) + + self.poll_url = '/sqleditor/poll/{0}'.format(self.trans_id) + + def _create_test_table(self): + create_sql = """ + DROP TABLE IF EXISTS test_for_updatable_resultset; + + CREATE TABLE test_for_updatable_resultset( + pk_col1 SERIAL, + pk_col2 SERIAL, + normal_col1 VARCHAR, + normal_col2 VARCHAR, + PRIMARY KEY(pk_col1, pk_col2) + ); + """ + + is_success, _ = \ + execute_query(tester=self.tester, + query=create_sql, + start_query_tool_url=self.start_query_tool_url, + poll_url=self.poll_url) + self.assertEquals(is_success, True) diff --git a/web/pgadmin/tools/sqleditor/tests/test_save_changed_data.py b/web/pgadmin/tools/sqleditor/tests/test_save_changed_data.py new file mode 100644 index 000000000..01795d291 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/tests/test_save_changed_data.py @@ -0,0 +1,347 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2019, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import json + +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from .execute_query_utils import execute_query + + +class TestSaveChangedData(BaseTestGenerator): + """ This class tests saving data changes in the grid to the database """ + scenarios = [ + ('When inserting new valid row', dict( + save_payload={ + "updated": {}, + "added": { + "2": { + "err": False, + "data": { + "pk_col": "3", + "__temp_PK": "2", + "normal_col": "three" + } + } + }, + "staged_rows": {}, + "deleted": {}, + "updated_index": {}, + "added_index": {"2": "2"}, + "columns": [ + { + "name": "pk_col", + "display_name": "pk_col", + "column_type": "[PK] integer", + "column_type_internal": "integer", + "pos": 0, + "label": "pk_col
[PK] integer", + "cell": "number", + "can_edit": True, + "type": "integer", + "not_null": True, + "has_default_val": False, + "is_array": False}, + {"name": "normal_col", + "display_name": "normal_col", + "column_type": "character varying", + "column_type_internal": "character varying", + "pos": 1, + "label": "normal_col
character varying", + "cell": "string", + "can_edit": True, + "type": "character varying", + "not_null": False, + "has_default_val": False, + "is_array": False} + ] + }, + save_status=True, + check_sql='SELECT * FROM test_for_save_data WHERE pk_col = 3', + check_result=[[3, "three"]] + )), + ('When inserting new invalid row', dict( + save_payload={ + "updated": {}, + "added": { + "2": { + "err": False, + "data": { + "pk_col": "1", + "__temp_PK": "2", + "normal_col": "four" + } + } + }, + "staged_rows": {}, + "deleted": {}, + "updated_index": {}, + "added_index": {"2": "2"}, + "columns": [ + { + "name": "pk_col", + "display_name": "pk_col", + "column_type": "[PK] integer", + "column_type_internal": "integer", + "pos": 0, + "label": "pk_col
[PK] integer", + "cell": "number", + "can_edit": True, + "type": "integer", + "not_null": True, + "has_default_val": False, + "is_array": False}, + {"name": "normal_col", + "display_name": "normal_col", + "column_type": "character varying", + "column_type_internal": "character varying", + "pos": 1, + "label": "normal_col
character varying", + "cell": "string", + "can_edit": True, + "type": "character varying", + "not_null": False, + "has_default_val": False, + "is_array": False} + ] + }, + save_status=False, + check_sql=None, + check_result=None + )), + ('When updating a row in a valid way', dict( + save_payload={ + "updated": { + "1": + {"err": False, + "data": {"normal_col": "ONE"}, + "primary_keys": + {"pk_col": 1} + } + }, + "added": {}, + "staged_rows": {}, + "deleted": {}, + "updated_index": {"1": "1"}, + "added_index": {}, + "columns": [ + { + "name": "pk_col", + "display_name": "pk_col", + "column_type": "[PK] integer", + "column_type_internal": "integer", + "pos": 0, + "label": "pk_col
[PK] integer", + "cell": "number", + "can_edit": True, + "type": "integer", + "not_null": True, + "has_default_val": False, + "is_array": False}, + {"name": "normal_col", + "display_name": "normal_col", + "column_type": "character varying", + "column_type_internal": "character varying", + "pos": 1, + "label": "normal_col
character varying", + "cell": "string", + "can_edit": True, + "type": "character varying", + "not_null": False, + "has_default_val": False, + "is_array": False} + ] + }, + save_status=True, + check_sql='SELECT * FROM test_for_save_data WHERE pk_col = 1', + check_result=[[1, "ONE"]] + )), + ('When updating a row in an invalid way', dict( + save_payload={ + "updated": { + "1": + {"err": False, + "data": {"pk_col": "2"}, + "primary_keys": + {"pk_col": 1} + } + }, + "added": {}, + "staged_rows": {}, + "deleted": {}, + "updated_index": {"1": "1"}, + "added_index": {}, + "columns": [ + { + "name": "pk_col", + "display_name": "pk_col", + "column_type": "[PK] integer", + "column_type_internal": "integer", + "pos": 0, + "label": "pk_col
[PK] integer", + "cell": "number", + "can_edit": True, + "type": "integer", + "not_null": True, + "has_default_val": False, + "is_array": False}, + {"name": "normal_col", + "display_name": "normal_col", + "column_type": "character varying", + "column_type_internal": "character varying", + "pos": 1, + "label": "normal_col
character varying", + "cell": "string", + "can_edit": True, + "type": "character varying", + "not_null": False, + "has_default_val": False, + "is_array": False} + ] + }, + save_status=False, + check_sql=None, + check_result=None + )), + ('When deleting a row', dict( + save_payload={ + "updated": {}, + "added": {}, + "staged_rows": {"1": {"pk_col": 2}}, + "deleted": {"1": {"pk_col": 2}}, + "updated_index": {}, + "added_index": {}, + "columns": [ + { + "name": "pk_col", + "display_name": "pk_col", + "column_type": "[PK] integer", + "column_type_internal": "integer", + "pos": 0, + "label": "pk_col
[PK] integer", + "cell": "number", + "can_edit": True, + "type": "integer", + "not_null": True, + "has_default_val": False, + "is_array": False}, + {"name": "normal_col", + "display_name": "normal_col", + "column_type": "character varying", + "column_type_internal": "character varying", + "pos": 1, + "label": "normal_col
character varying", + "cell": "string", + "can_edit": True, + "type": "character varying", + "not_null": False, + "has_default_val": False, + "is_array": False} + ] + }, + save_status=True, + check_sql='SELECT * FROM test_for_save_data WHERE pk_col = 2', + check_result='SELECT 0' + )), + ] + + def setUp(self): + self._initialize_database_connection() + self._initialize_query_tool() + self._initialize_urls_and_select_sql() + self._create_test_table() + + def runTest(self): + # Execute select sql + is_success, _ = \ + execute_query(tester=self.tester, + query=self.select_sql, + start_query_tool_url=self.start_query_tool_url, + poll_url=self.poll_url) + self.assertEquals(is_success, True) + + # Send a request to save changed data + response = self.tester.post(self.save_url, + data=json.dumps(self.save_payload), + content_type='html/json') + + self.assertEquals(response.status_code, 200) + + # Check that the save is successful + response_data = json.loads(response.data.decode('utf-8')) + save_status = response_data['data']['status'] + self.assertEquals(save_status, self.save_status) + + if self.check_sql: + # Execute check sql + is_success, response_data = \ + execute_query(tester=self.tester, + query=self.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) + + def tearDown(self): + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) + + def _initialize_database_connection(self): + database_info = parent_node_dict["database"][-1] + self.server_id = database_info["server_id"] + + self.db_id = database_info["db_id"] + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if not db_con["info"] == "Database connected.": + raise Exception("Could not connect to the database.") + + def _initialize_query_tool(self): + url = '/datagrid/initialize/query_tool/{0}/{1}/{2}'.format( + utils.SERVER_GROUP, self.server_id, self.db_id) + response = self.tester.post(url) + self.assertEquals(response.status_code, 200) + + response_data = json.loads(response.data.decode('utf-8')) + self.trans_id = response_data['data']['gridTransId'] + + def _initialize_urls_and_select_sql(self): + self.start_query_tool_url = \ + '/sqleditor/query_tool/start/{0}'.format(self.trans_id) + self.save_url = '/sqleditor/save/{0}'.format(self.trans_id) + self.poll_url = '/sqleditor/poll/{0}'.format(self.trans_id) + + self.select_sql = 'SELECT * FROM test_for_save_data;' + + def _create_test_table(self): + create_sql = """ + DROP TABLE IF EXISTS test_for_save_data; + + CREATE TABLE test_for_save_data( + pk_col INT PRIMARY KEY, + normal_col VARCHAR); + + INSERT INTO test_for_save_data VALUES + (1, 'one'), + (2, 'two'); + """ + is_success, _ = \ + execute_query(tester=self.tester, + query=create_sql, + start_query_tool_url=self.start_query_tool_url, + poll_url=self.poll_url) + self.assertEquals(is_success, True) diff --git a/web/pgadmin/tools/sqleditor/utils/is_query_resultset_updatable.py b/web/pgadmin/tools/sqleditor/utils/is_query_resultset_updatable.py new file mode 100644 index 000000000..f6b453ee9 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/is_query_resultset_updatable.py @@ -0,0 +1,120 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2019, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +""" + Check if the result-set of a query is updatable, A resultset is + updatable (as of this version) if: + - All columns belong to the same table. + - All the primary key columns of the table are present in the resultset + - No duplicate columns +""" +from flask import render_template +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict + + +def is_query_resultset_updatable(conn, sql_path): + """ + This function is used to check whether the last successful query + produced updatable results. + + Args: + conn: Connection object. + sql_path: the path to the sql templates. + """ + columns_info = conn.get_column_info() + + if columns_info is None or len(columns_info) < 1: + return return_not_updatable() + + table_oid = _check_single_table(columns_info) + if not table_oid: + return return_not_updatable() + + if not _check_duplicate_columns(columns_info): + return return_not_updatable() + + if conn.connected(): + primary_keys, primary_keys_columns, pk_names = \ + _get_primary_keys(conn=conn, + table_oid=table_oid, + sql_path=sql_path) + + if not _check_primary_keys_uniquely_exist(primary_keys_columns, + columns_info): + return return_not_updatable() + + return True, primary_keys, pk_names, table_oid + else: + return return_not_updatable() + + +def _check_single_table(columns_info): + table_oid = columns_info[0]['table_oid'] + for column in columns_info: + if column['table_oid'] != table_oid: + return None + return table_oid + + +def _check_duplicate_columns(columns_info): + column_numbers = \ + [col['table_column'] for col in columns_info] + is_duplicate_columns = len(column_numbers) != len(set(column_numbers)) + if is_duplicate_columns: + return False + return True + + +def _check_primary_keys_uniquely_exist(primary_keys_columns, columns_info): + for pk in primary_keys_columns: + pk_exists = False + for col in columns_info: + if col['table_column'] == pk['column_number']: + pk_exists = True + # If the primary key column is renamed + if col['display_name'] != pk['name']: + return False + # If a normal column is renamed to a primary key column name + elif col['display_name'] == pk['name']: + return False + + if not pk_exists: + return False + return True + + +def _get_primary_keys(sql_path, table_oid, conn): + query = render_template( + "/".join([sql_path, 'primary_keys.sql']), + obj_id=table_oid + ) + status, result = conn.execute_dict(query) + if not status: + return return_not_updatable() + + primary_keys_columns = [] + primary_keys = OrderedDict() + pk_names = [] + + for row in result['rows']: + primary_keys[row['attname']] = row['typname'] + primary_keys_columns.append({ + 'name': row['attname'], + 'column_number': row['attnum'] + }) + pk_names.append(row['attname']) + + return primary_keys, primary_keys_columns, pk_names + + +def return_not_updatable(): + return False, None, None, None diff --git a/web/pgadmin/tools/sqleditor/utils/save_changed_data.py b/web/pgadmin/tools/sqleditor/utils/save_changed_data.py new file mode 100644 index 000000000..6275e6585 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/save_changed_data.py @@ -0,0 +1,317 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2019, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from flask import render_template +from pgadmin.tools.sqleditor.utils.constant_definition import TX_STATUS_IDLE +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict + + +def save_changed_data(changed_data, columns_info, conn, command_obj, + client_primary_key, auto_commit=True): + """ + This function is used to save the data into the database. + Depending on condition it will either update or insert the + new row into the database. + + Args: + changed_data: Contains data to be saved + command_obj: The transaction object (command_obj or trans_obj) + conn: The connection object + columns_info: session_obj['columns_info'] + client_primary_key: session_obj['client_primary_key'] + auto_commit: If the changes should be commited automatically. + """ + status = False + res = None + query_res = dict() + count = 0 + list_of_rowid = [] + operations = ('added', 'updated', 'deleted') + list_of_sql = {} + _rowid = None + is_commit_required = False + + pgadmin_alias = { + col_name: col_info['pgadmin_alias'] + 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]) + ) + ) + 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 + ) + + 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( + "/".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': {}}) + + 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.' + res += ' Saving 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,\ + is_commit_required + + 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 + + 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,\ + is_commit_required + + 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;') + else: + is_commit_required = True + + return status, res, query_res, _rowid, is_commit_required diff --git a/web/regression/re_sql/tests/test_resql.py b/web/regression/re_sql/tests/test_resql.py index b4b4aee81..697e2df38 100644 --- a/web/regression/re_sql/tests/test_resql.py +++ b/web/regression/re_sql/tests/test_resql.py @@ -96,69 +96,8 @@ class ReverseEngineeredSQLTestCases(BaseTestGenerator): filename) with open(complete_file_name) as jsonfp: data = json.load(jsonfp) - # CHECK SERVER VERSION & TYPE PRECONDITION - flag = False - if 'prerequisite' in data and \ - data['prerequisite'] is not None: - prerequisite_data = data['prerequisite'] - - module_str = module.replace('_', ' ').capitalize() - db_type = server_info['type'].upper() - min_ver = prerequisite_data['minVer'] - max_ver = prerequisite_data['maxVer'] - - if 'type' in prerequisite_data and \ - prerequisite_data['type']: - if server_info['type'] != \ - prerequisite_data['type']: - flag = True - print( - "\n\n" - "{0} are not supported by {1} - " - "Skipped".format( - module_str, - db_type - ), - file=sys.stderr - ) - - if 'minVer' in prerequisite_data and \ - prerequisite_data['minVer']: - if server_info['server_version'] < \ - prerequisite_data['minVer']: - if not flag: - flag = True - print( - "\n\n" - "{0} are not supported by" - " {1} server less than" - " {2} - Skipped".format( - module_str, db_type, min_ver - ), - file=sys.stderr - ) - - if 'maxVer' in prerequisite_data and \ - prerequisite_data['maxVer']: - if server_info['server_version'] > \ - prerequisite_data['maxVer']: - if not flag: - flag = True - print( - "\n\n" - "{0} are not supported by" - " {1} server greater than" - " {2} - Skipped".format( - module_str, db_type, max_ver - ), - file=sys.stderr - ) - - if not flag: - tests_scenarios = {} - tests_scenarios['scenarios'] = data['scenarios'] - for key, scenarios in tests_scenarios.items(): - self.execute_test_case(scenarios) + for key, scenarios in data.items(): + self.execute_test_case(scenarios) def tearDown(self): database_utils.disconnect_database( @@ -209,7 +148,7 @@ class ReverseEngineeredSQLTestCases(BaseTestGenerator): for scenario in scenarios: print(scenario['name']) - if scenario['data'] and 'schema' in scenario['data']: + if 'data' in scenario and 'schema' in scenario['data']: # If schema is already exist then fetch the oid schema = regression.schema_utils.verify_schemas( self.server, self.db_name, @@ -265,8 +204,16 @@ class ReverseEngineeredSQLTestCases(BaseTestGenerator): :param module_path: Path of the module to be tested. :return: """ - # Join the application path and the module path - absolute_path = os.path.join(self.apppath, module_path) + # Join the application path, module path and tests folder + tests_folder_path = os.path.join(self.apppath, module_path, 'tests') + + # A folder name matching the Server Type (pg, ppas) takes priority so + # check whether that exists or not. If so, than check the version + # folder in it, else look directly in the 'tests' folder. + absolute_path = os.path.join(tests_folder_path, self.server['type']) + if not os.path.exists(absolute_path): + absolute_path = tests_folder_path + # Iterate the version mapping directories. for version_mapping in get_version_mapping_directories( self.server['type']): @@ -274,7 +221,7 @@ class ReverseEngineeredSQLTestCases(BaseTestGenerator): self.server_information['server_version']: continue - complete_path = os.path.join(absolute_path, 'tests', + complete_path = os.path.join(absolute_path, version_mapping['name']) if os.path.exists(complete_path):