diff --git a/docs/en_US/compound_trigger_dialog.rst b/docs/en_US/compound_trigger_dialog.rst index f50dbfd9b..f8e0f21d9 100644 --- a/docs/en_US/compound_trigger_dialog.rst +++ b/docs/en_US/compound_trigger_dialog.rst @@ -34,7 +34,8 @@ Use the fields in the *Events* tab to specify how and when the compound trigger * Select the type of event(s) that will invoke the compound trigger; to select an event type, move the switch next to the event to the *YES* position. - The supported event types are *INSERT*, *UPDATE*, *DELETE*. + The supported event types are *INSERT*, *UPDATE*, *DELETE* and *TRUNCATE*. + Views cannot have TRUNCATE triggers. * Use the *When* field to provide a boolean condition that will invoke the compound trigger. * If defining a column-specific compound trigger, use the *Columns* field to @@ -46,10 +47,10 @@ Click the *Code* tab to continue. :alt: Compound Trigger dialog code tab :align: center -Use the *Code* field to specify the code for the four timing events -*BEFORE STATEMENT*, *AFTER STATEMENT*, *BEFORE EACH ROW*, *AFTER EACH ROW* -that will be invoked when the compound trigger fires. Basic template is provided -with place holders. +Use the *Code* field to specify the code for the five timing events +*BEFORE STATEMENT*, *AFTER STATEMENT*, *BEFORE EACH ROW*, *AFTER EACH ROW*, +*INSTEAD OF EACH ROW* that will be invoked when the compound trigger fires. +Basic template is provided with place holders. Click the *SQL* tab to continue. diff --git a/docs/en_US/images/compound_trigger_events.png b/docs/en_US/images/compound_trigger_events.png index 6c37a9c68..676ec9983 100644 Binary files a/docs/en_US/images/compound_trigger_events.png and b/docs/en_US/images/compound_trigger_events.png differ diff --git a/docs/en_US/release_notes_4_12.rst b/docs/en_US/release_notes_4_12.rst index 5e59eb452..49c66084c 100644 --- a/docs/en_US/release_notes_4_12.rst +++ b/docs/en_US/release_notes_4_12.rst @@ -52,4 +52,6 @@ Bug fixes | `Issue #4582 `_ - Fix console error when changing kind(SQL/BATCH) for pgAgent job step. | `Issue #4585 `_ - Fix double click issue to expand the contents of a cell if the resultset was not editable. | `Issue #4586 `_ - Fix generation of reverse engineered SQL for Rules. -| `Issue #4635 `_ - Ensure compound triggers for event should be updated properly. \ No newline at end of file +| `Issue #4635 `_ - Ensure compound triggers for event should be updated properly. +| `Issue #4638 `_ - Ensure compound triggers should be displayed under Views. +| `Issue #4641 `_ - Ensure Truncate option should be available for Compound Triggers. \ No newline at end of file diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/static/js/compound_trigger.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/static/js/compound_trigger.js index 109c33f24..2c30d1bcd 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/static/js/compound_trigger.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/static/js/compound_trigger.js @@ -185,7 +185,13 @@ define('pgadmin.node.compound_trigger', [ type: 'int', disabled: true, mode: ['properties'], },{ id: 'is_enable_trigger', label: gettext('Trigger enabled?'), - type: 'switch', disabled: 'inSchema', mode: ['edit', 'properties'], + type: 'switch', mode: ['edit', 'properties'], + disabled: function() { + if(this.node_info && ('catalog' in this.node_info || 'view' in this.node_info)) { + return true; + } + return false; + }, },{ type: 'nested', control: 'fieldset', mode: ['create','edit', 'properties'], label: gettext('FOR Events'), group: gettext('Events'), contentClass: 'row', @@ -228,6 +234,23 @@ define('pgadmin.node.compound_trigger', [ return false; return m.inSchemaWithModelCheck.apply(this, [m]); }, + },{ + id: 'evnt_truncate', label: gettext('TRUNCATE'), + type: 'switch', mode: ['create','edit', 'properties'], + group: gettext('FOR Events'), + extraToggleClasses: 'pg-el-sm-6', + controlLabelClassName: 'control-label pg-el-sm-5 pg-el-12', + controlsClassName: 'pgadmin-controls pg-el-sm-7 pg-el-12', + disabled: function(m) { + var evn_truncate = m.get('evnt_truncate'); + // Views cannot have TRUNCATE triggers. + if ('view' in m.node_info) + return true; + + if (!_.isUndefined(evn_truncate) && m.node_info['server']['server_type'] == 'ppas') + return false; + return m.inSchemaWithModelCheck.apply(this, [m]); + }, }], },{ id: 'whenclause', label: gettext('When'), @@ -337,6 +360,12 @@ define('pgadmin.node.compound_trigger', [ ' -- Enter any local declarations here\n' + 'BEGIN\n' + ' -- Enter any required code here\n' + + 'END;\n\n' + + '-- INSTEAD OF EACH ROW block. Delete if not required.\n' + + 'INSTEAD OF EACH ROW IS\n' + + ' -- Enter any local declarations here\n' + + 'BEGIN\n' + + ' -- Enter any required code here\n' + 'END;'); }, // We will check if we are under schema node & in 'create' mode @@ -397,11 +426,21 @@ define('pgadmin.node.compound_trigger', [ }, // Check to whether trigger is disable ? canCreate_with_compound_trigger_enable: function(itemData, item, data) { + var treeData = this.getTreeNodeHierarchy(item); + if ('view' in treeData) { + return false; + } + return itemData.icon === 'icon-compound_trigger-bad' && this.canCreate.apply(this, [itemData, item, data]); }, // Check to whether trigger is enable ? canCreate_with_compound_trigger_disable: function(itemData, item, data) { + var treeData = this.getTreeNodeHierarchy(item); + if ('view' in treeData) { + return false; + } + return itemData.icon === 'icon-compound_trigger' && this.canCreate.apply(this, [itemData, item, data]); }, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/tests/ppas/12_plus/create_for_all_event.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/tests/ppas/12_plus/create_for_all_event.sql new file mode 100644 index 000000000..257da20ab --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/tests/ppas/12_plus/create_for_all_event.sql @@ -0,0 +1,16 @@ +-- Compound Trigger: test_compound_trigger_$%{}[]()&*^!@"'`\/# + +-- DROP TRIGGER "test_compound_trigger_$%{}[]()&*^!@""'`\/#" ON testschema.table_for_compound_trigger; + +CREATE OR REPLACE TRIGGER "test_compound_trigger_$%{}[]()&*^!@""'`\/#" + FOR INSERT OR DELETE OR TRUNCATE OR UPDATE + ON testschema.table_for_compound_trigger + COMPOUND TRIGGER +var character varying(20) DEFAULT 'Global_var'; + +BEFORE STATEMENT IS +BEGIN + DBMS_OUTPUT.PUT_LINE('Before Statement: ' || var); + var := 'BEFORE STATEMENT'; +END; +END "test_compound_trigger_$%{}[]()&*^!@""'`\/#"; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/tests/ppas/12_plus/test.json b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/tests/ppas/12_plus/test.json index 9037c195f..e33be7670 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/tests/ppas/12_plus/test.json +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/tests/ppas/12_plus/test.json @@ -94,6 +94,27 @@ "data": { "name": "test_compound_trigger_$%{}[]()&*^!@\"'`\\/#" } + }, { + "type": "create", + "name": "Create compound trigger for all event", + "endpoint": "NODE-compound_trigger.obj", + "sql_endpoint": "NODE-compound_trigger.sql_id", + "data": { + "name": "test_compound_trigger_$%{}[]()&*^!@\"'`\\/#", + "prosrc": "var varchar2(20) := 'Global_var';\n\nBEFORE STATEMENT IS\nBEGIN\n\tDBMS_OUTPUT.PUT_LINE('Before Statement: ' || var);\n\tvar := 'BEFORE STATEMENT';\nEND;", + "evnt_insert": true, + "evnt_update": true, + "evnt_delete": true, + "evnt_truncate": true + }, + "expected_sql_file": "create_for_all_event.sql" + }, { + "type": "delete", + "name": "Drop Compound Trigger", + "endpoint": "NODE-compound_trigger.delete_id", + "data": { + "name": "test_compound_trigger_$%{}[]()&*^!@\"'`\\/#" + } } ] } diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/tests/test_compound_triggers_add.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/tests/test_compound_triggers_add.py index ba839aeef..a7969d26b 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/tests/test_compound_triggers_add.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/tests/test_compound_triggers_add.py @@ -20,12 +20,28 @@ from pgadmin.browser.server_groups.servers.databases.tests import utils as \ from pgadmin.utils.route import BaseTestGenerator from regression import parent_node_dict from regression.python_test_utils import test_utils as utils +from pgadmin.browser.server_groups.servers.databases.schemas.views.tests \ + import utils as view_utils class CompoundTriggersAddTestCase(BaseTestGenerator): """This class will add new compound trigger under table node.""" skip_on_database = ['gpdb'] scenarios = [ + ('Create compound trigger for all events', + dict( + url='/browser/compound_trigger/obj/', + data={ + "prosrc": "var varchar2(20) := 'Global_var';\n\n" + "BEFORE STATEMENT IS\nBEGIN\n " + "DBMS_OUTPUT.PUT_LINE('Before Statement: ' || var)" + ";\n var := 'BEFORE STATEMENT';\nEND;", + "evnt_insert": True, + "evnt_update": True, + "evnt_delete": True, + "evnt_truncate": True + } + )), ('Create compound trigger for insert and delete', dict( url='/browser/compound_trigger/obj/', @@ -75,6 +91,47 @@ class CompoundTriggersAddTestCase(BaseTestGenerator): "columns": ["id", "name"] } )), + ('Create compound trigger for truncate', + dict( + url='/browser/compound_trigger/obj/', + data={ + "prosrc": "var varchar2(20) := 'Global_var';\n\n" + "BEFORE STATEMENT IS\nBEGIN\n " + "DBMS_OUTPUT.PUT_LINE('Before Statement: ' || var)" + ";\n var := 'BEFORE STATEMENT';\nEND;", + "evnt_truncate": True + } + )), + ('Create compound trigger for insert delete and update on view', + dict( + url='/browser/compound_trigger/obj/', + data={ + "prosrc": "var varchar2(20) := 'Global_var';\n\n" + "BEFORE STATEMENT IS\nBEGIN\n " + "DBMS_OUTPUT.PUT_LINE('Before Statement: ' || var)" + ";\n var := 'BEFORE STATEMENT';\nEND;", + "evnt_insert": True, + "evnt_update": True, + "evnt_delete": True, + "evnt_truncate": False + }, + on_view=True + )), + ('Create compound trigger for instead of each row', + dict( + url='/browser/compound_trigger/obj/', + data={ + "prosrc": "var varchar2(20) := 'Global_var';\n\n" + "INSTEAD OF EACH ROW IS\nBEGIN\n " + "DBMS_OUTPUT.PUT_LINE('Instead of: ' || var)" + ";\n var := 'INSTEAD OF EACH ROW';\nEND;", + "evnt_insert": True, + "evnt_update": True, + "evnt_delete": True, + "evnt_truncate": False + }, + on_view=True + )), ] def setUp(self): @@ -112,6 +169,14 @@ class CompoundTriggersAddTestCase(BaseTestGenerator): self.table_id = tables_utils.create_table(self.server, self.db_name, self.schema_name, self.table_name) + view_sql = "CREATE OR REPLACE VIEW %s.%s AS SELECT 'Hello World'; " \ + "ALTER TABLE %s.%s OWNER TO %s" + self.view_name = \ + "view_compound_trigger_%s" % (str(uuid.uuid4())[1:8]) + self.view_id = view_utils.create_view(self.server, self.db_name, + self.schema_name, + view_sql, + self.view_name) def runTest(self): """This function will create compound trigger under table node.""" @@ -120,10 +185,14 @@ class CompoundTriggersAddTestCase(BaseTestGenerator): self.data.update({"name": trigger_name}) + object_id = self.table_id + if hasattr(self, 'on_view'): + object_id = self.view_id + response = self.tester.post( "{0}{1}/{2}/{3}/{4}/{5}/".format(self.url, utils.SERVER_GROUP, self.server_id, self.db_id, - self.schema_id, self.table_id), + self.schema_id, object_id), data=json.dumps(self.data), content_type='html/json' ) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/compound_triggers/sql/ppas/12_plus/update.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/compound_triggers/sql/ppas/12_plus/update.sql index 8bced4b8c..51308dab7 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/compound_triggers/sql/ppas/12_plus/update.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/compound_triggers/sql/ppas/12_plus/update.sql @@ -9,7 +9,10 @@ CREATE OR REPLACE TRIGGER {{ conn|qtIdent(data.name) }} FOR {% if data.evnt_insert is not defined %}{% if o_data.evnt_insert %}INSERT{% set or_flag = True %}{% endif %}{% else %}{% if data.evnt_insert %}INSERT{% set or_flag = True %}{% endif %}{% endif %}{% if data.evnt_delete is not defined %}{% if o_data.evnt_delete %} {% if or_flag %} OR {% endif %}DELETE{% set or_flag = True %} {% endif %}{% else %}{% if data.evnt_delete %} -{% if or_flag %} OR {% endif %}DELETE{% set or_flag = True %}{%endif %}{% endif %}{% if data.evnt_update is not defined %}{% if o_data.evnt_update %} +{% if or_flag %} OR {% endif %}DELETE{% set or_flag = True %}{%endif %}{% endif %}{% if data.evnt_truncate is not defined %}{% if o_data.evnt_truncate %} +{% if or_flag %} OR {% endif %}TRUNCATE{% set or_flag = True %} +{% endif %}{% else %}{% if data.evnt_truncate %} +{% if or_flag %} OR {% endif %}TRUNCATE{% set or_flag = True %}{%endif %}{% endif %}{% if data.evnt_update is not defined %}{% if o_data.evnt_update %} {% if or_flag %} OR {% endif %}UPDATE{% if o_data.columns|length > 0 %} OF {% for c in o_data.columns %}{% if loop.index != 1 %}, {% endif %}{{ conn|qtIdent(c) }}{% endfor %}{% endif %} {% endif %}{% else %}{% if data.evnt_update %} {% if or_flag %} OR {% endif %}UPDATE{% if o_data.columns|length > 0 %} OF {% for c in o_data.columns %}{% if loop.index != 1 %}, {% endif %}{{ conn|qtIdent(c) }}{% endfor %}{% endif %}{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js index 5745ed5f9..1a01268ab 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js @@ -187,8 +187,13 @@ define('pgadmin.node.trigger', [ type: 'int', disabled: true, mode: ['properties'], },{ id: 'is_enable_trigger', label: gettext('Trigger enabled?'), - type: 'switch', disabled: 'inSchema', mode: ['edit', 'properties'], - group: gettext('Definition'), + type: 'switch', mode: ['edit', 'properties'], group: gettext('Definition'), + disabled: function() { + if(this.node_info && ('catalog' in this.node_info || 'view' in this.node_info)) { + return true; + } + return false; + }, },{ id: 'is_row_trigger', label: gettext('Row trigger?'), type: 'switch', group: gettext('Definition'), @@ -629,11 +634,21 @@ define('pgadmin.node.trigger', [ canCreate: SchemaChildTreeNode.isTreeItemOfChildOfSchema, // Check to whether trigger is disable ? canCreate_with_trigger_enable: function(itemData, item, data) { + var treeData = this.getTreeNodeHierarchy(item); + if ('view' in treeData) { + return false; + } + return itemData.icon === 'icon-trigger-bad' && this.canCreate.apply(this, [itemData, item, data]); }, // Check to whether trigger is enable ? canCreate_with_trigger_disable: function(itemData, item, data) { + var treeData = this.getTreeNodeHierarchy(item); + if ('view' in treeData) { + return false; + } + return itemData.icon === 'icon-trigger' && this.canCreate.apply(this, [itemData, item, data]); }, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/__init__.py index 12442a26c..cfefa5314 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/__init__.py @@ -874,6 +874,83 @@ class ViewNode(PGChildNodeView, VacuumSettings): SQL_data += SQL return SQL_data + def get_compound_trigger_sql(self, vid): + """ + Get all compound trigger nodes associated with view node, + generate their sql and render into sql tab + """ + if self.manager.server_type == 'ppas' \ + and self.manager.version >= 120000: + + from pgadmin.browser.server_groups.servers.databases.schemas.utils\ + import trigger_definition + + # Define template path + self.ct_trigger_temp_path = 'compound_triggers' + + SQL_data = '' + SQL = render_template("/".join( + [self.ct_trigger_temp_path, + 'sql/{0}/#{1}#/nodes.sql'.format( + self.manager.server_type, self.manager.version)]), + tid=vid) + + status, data = self.conn.execute_dict(SQL) + if not status: + return internal_server_error(errormsg=data) + + for trigger in data['rows']: + SQL = render_template("/".join( + [self.ct_trigger_temp_path, + 'sql/{0}/#{1}#/properties.sql'.format( + self.manager.server_type, self.manager.version)]), + tid=vid, + trid=trigger['oid'] + ) + + status, res = self.conn.execute_dict(SQL) + if not status: + return internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + continue + res_rows = dict(res['rows'][0]) + res_rows['table'] = res_rows['relname'] + res_rows['schema'] = self.view_schema + + if len(res_rows['tgattr']) > 1: + columns = ', '.join(res_rows['tgattr'].split(' ')) + SQL = render_template("/".join( + [self.ct_trigger_temp_path, + 'sql/{0}/#{1}#/get_columns.sql'.format( + self.manager.server_type, + self.manager.version)]), + tid=trigger['oid'], + clist=columns) + + status, rset = self.conn.execute_2darray(SQL) + if not status: + return internal_server_error(errormsg=rset) + # 'tgattr' contains list of columns from table + # used in trigger + columns = [] + + for col_row in rset['rows']: + columns.append(col_row['name']) + + res_rows['columns'] = columns + + res_rows = trigger_definition(res_rows) + SQL = render_template("/".join( + [self.ct_trigger_temp_path, + 'sql/{0}/#{1}#/create.sql'.format( + self.manager.server_type, self.manager.version)]), + data=res_rows, display_comments=True) + SQL_data += '\n' + SQL_data += SQL + + return SQL_data + def get_trigger_sql(self, vid): """ Get all trigger nodes associated with view node, @@ -943,7 +1020,8 @@ class ViewNode(PGChildNodeView, VacuumSettings): if not status: return internal_server_error(errormsg=result) - # Update the trigger function which we have fetched with schemaname + # Update the trigger function which we have fetched with + # schemaname if ( 'rows' in result and len(result['rows']) > 0 and 'tfunctions' in result['rows'][0] @@ -1083,6 +1161,7 @@ class ViewNode(PGChildNodeView, VacuumSettings): SQL_data += SQL SQL_data += self.get_rule_sql(vid) SQL_data += self.get_trigger_sql(vid) + SQL_data += self.get_compound_trigger_sql(vid) SQL_data += self.get_index_sql(did, vid) return ajax_response(response=SQL_data) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/children/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/children/__init__.py index b99b96a56..e0f13e21f 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/children/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/children/__init__.py @@ -13,3 +13,5 @@ from pgadmin.browser.server_groups.servers.databases.schemas.tables.triggers \ import blueprint as triggers_modules from pgadmin.browser.server_groups.servers.databases.schemas.tables.rules \ import blueprint as rules_modules +from pgadmin.browser.server_groups.servers.databases.schemas.tables.\ + compound_triggers import blueprint as compound_trigger_modules