diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/publications/__init__.py new file mode 100644 index 000000000..cdc0ea7d1 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/__init__.py @@ -0,0 +1,790 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Implements Publication Node""" +import simplejson as json +from functools import wraps + +import pgadmin.browser.server_groups.servers.databases as databases +from flask import render_template, request, jsonify +from flask_babelex import gettext +from pgadmin.browser.collection import CollectionNodeModule +from pgadmin.browser.utils import PGChildNodeView +from pgadmin.utils.ajax import make_json_response, internal_server_error, \ + make_response as ajax_response, gone +from pgadmin.utils.driver import get_driver +from config import PG_DEFAULT_DRIVER +from pgadmin.tools.schema_diff.compare import SchemaDiffObjectCompare + + +class PublicationModule(CollectionNodeModule): + """ + class PublicationModule(CollectionNodeModule) + + A module class for Publication node derived from CollectionNodeModule. + + Methods: + ------- + * __init__(*args, **kwargs) + - Method is used to initialize the PublicationModule and it's + base module. + + * get_nodes(gid, sid, did) + - Method is used to generate the browser collection node. + + * node_inode() + - Method is overridden from its base class to make the node as leaf node. + + * script_load() + - Load the module script for publication, when any of the database node + is initialized. + """ + + _NODE_TYPE = 'publication' + _COLLECTION_LABEL = gettext("Publications") + + def __init__(self, *args, **kwargs): + """ + Method is used to initialize the PublicationModule and it's + base module. + + Args: + *args: + **kwargs: + """ + super(PublicationModule, self).__init__(*args, **kwargs) + self.min_ver = 100000 + self.max_ver = None + + def get_nodes(self, gid, sid, did): + """ + Method is used to generate the browser collection node + + Args: + gid: Server Group ID + sid: Server ID + did: Database Id + """ + yield self.generate_browser_collection_node(did) + + @property + def node_inode(self): + """ + Override this property to make the node a leaf node. + + Returns: False as this is the leaf node + """ + return False + + @property + def script_load(self): + """ + Load the module script for publication, when any of the database nodes + are initialized. + + Returns: node type of the server module. + """ + return databases.DatabaseModule.node_type + + @property + def module_use_template_javascript(self): + """ + Returns whether Jinja2 template is used for generating the javascript + module. + """ + return False + + +blueprint = PublicationModule(__name__) + + +class PublicationView(PGChildNodeView, SchemaDiffObjectCompare): + """ + class PublicationView(PGChildNodeView) + + A view class for Publication node derived from PGChildNodeView. + This class is responsible for all the stuff related to view like + updating publication node, showing properties, showing sql in sql pane. + + Methods: + ------- + * __init__(**kwargs) + - Method is used to initialize the PublicationView and it's base view. + + * check_precondition() + - This function will behave as a decorator which will checks + database connection before running view, it will also attaches + manager,conn & template_path properties to self + + * list() + - This function is used to list all the publication nodes within that + collection. + + * nodes() + - This function will used to create all the child node within that + collection. Here it will create all the publication node. + + * properties(gid, sid, did, pbid) + - This function will show the properties of the selected publication node + + * update(gid, sid, did, pbid) + - This function will update the data for the selected publication node + + * create(gid, sid, did) + - This function will create the new publication node + + * delete(gid, sid, did, pbid) + - This function will delete the selected publication node + + * msql(gid, sid, did, pbid) + - This function is used to return modified SQL for the selected + publication node + + * get_sql(data, pbid) + - This function will generate sql from model data + + * get_tables(gid, sid, did) + - This function returns the handler and inline functions for the + selected publication node + + * get_templates(gid, sid, did) + - This function returns publication templates. + + * sql(gid, sid, did, pbid): + - This function will generate sql to show it in sql pane for the + selected publication node. + + * dependents(gid, sid, did, pbid): + - This function get the dependents and return ajax response for the + publication node. + + * dependencies(self, gid, sid, did, pbid): + - This function get the dependencies and return ajax response for the + publication node. + """ + + _NOT_FOUND_PUB_INFORMATION = \ + gettext("Could not find the publication information.") + node_type = blueprint.node_type + + parent_ids = [ + {'type': 'int', 'id': 'gid'}, + {'type': 'int', 'id': 'sid'}, + {'type': 'int', 'id': 'did'} + ] + ids = [ + {'type': 'int', 'id': 'pbid'} + ] + + operations = dict({ + 'obj': [ + {'get': 'properties', 'delete': 'delete', 'put': 'update'}, + {'get': 'list', 'post': 'create', 'delete': 'delete'} + ], + 'nodes': [{'get': 'node'}, {'get': 'nodes'}], + 'sql': [{'get': 'sql'}], + 'msql': [{'get': 'msql'}, {'get': 'msql'}], + 'stats': [{'get': 'statistics'}], + 'dependency': [{'get': 'dependencies'}], + 'dependent': [{'get': 'dependents'}], + 'get_tables': [{}, {'get': 'get_tables'}], + 'get_templates': [{}, {'get': 'get_templates'}], + 'delete': [{'delete': 'delete'}, {'delete': 'delete'}] + }) + + def _init_(self, **kwargs): + """ + Method is used to initialize the PublicationView and its base view. + Initialize all the variables create/used dynamically like conn, + template_path. + + Args: + **kwargs: + """ + self.conn = None + self.template_path = None + self.manager = None + + super(PublicationView, self).__init__(**kwargs) + + def check_precondition(f): + """ + This function will behave as a decorator which will check the + database connection before running the view. It also attaches + manager, conn & template_path properties to self + """ + + @wraps(f) + def wrap(*args, **kwargs): + # Here args[0] will hold self & kwargs will hold gid,sid,did + self = args[0] + self.driver = get_driver(PG_DEFAULT_DRIVER) + self.manager = self.driver.connection_manager(kwargs['sid']) + self.conn = self.manager.connection(did=kwargs['did']) + self.datlastsysoid = self.manager.db_info[kwargs['did']][ + 'datlastsysoid'] if self.manager.db_info is not None \ + and kwargs['did'] in self.manager.db_info else 0 + + # Set the template path for the SQL scripts + self.template_path = ( + "publications/sql/#gpdb#{0}#".format(self.manager.version) if + self.manager.server_type == 'gpdb' else + "publications/sql/#{0}#".format(self.manager.version) + ) + + return f(*args, **kwargs) + + return wrap + + @staticmethod + def _parser_data_input_from_client(data): + """ + + :param data: + :return: data + """ + + if 'pubtable' in data and data['pubtable'] != '': + data['pubtable'] = json.loads( + data['pubtable'], encoding='utf-8' + ) + return data + + @check_precondition + def list(self, gid, sid, did): + """ + This function is used to list all the publication nodes within that + collection. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + """ + sql = render_template("/".join([self.template_path, + self._PROPERTIES_SQL])) + status, res = self.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=res) + return ajax_response( + response=res['rows'], + status=200 + ) + + @check_precondition + def nodes(self, gid, sid, did): + """ + This function is used to create all the child nodes within the + collection. Here it will create all the publication nodes. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + """ + res = [] + sql = render_template("/".join([self.template_path, + 'nodes.sql'])) + status, result = self.conn.execute_2darray(sql) + if not status: + return internal_server_error(errormsg=result) + + for row in result['rows']: + res.append( + self.blueprint.generate_browser_node( + row['oid'], + did, + row['name'], + icon="icon-publication" + )) + + return make_json_response( + data=res, + status=200 + ) + + @check_precondition + def node(self, gid, sid, did, pbid): + """ + This function will fetch properties of the publication nodes. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + pbid: Publication ID + """ + sql = render_template("/".join([self.template_path, + self._PROPERTIES_SQL]), + pbid=pbid) + status, result = self.conn.execute_2darray(sql) + if not status: + return internal_server_error(errormsg=result) + + for row in result['rows']: + return make_json_response( + data=self.blueprint.generate_browser_node( + row['oid'], + did, + row['name'], + icon="icon-publication" + ), + status=200 + ) + + return gone(gettext("Could not find the specified publication.")) + + @check_precondition + def properties(self, gid, sid, did, pbid): + """ + This function will show the properties of the + selected publication node. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + pbid: Publication ID + """ + status, res = self._fetch_properties(did, pbid) + + if not status: + return res + + if 'all_table' in res and res['all_table']: + res['pubtable'] = '' + + return ajax_response( + response=res, + status=200 + ) + + def _fetch_properties(self, did, pbid): + """ + This function fetch the properties of the extension. + :param did: + :param pbid: + :return: + """ + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + pbid=pbid + ) + + status, res = self.conn.execute_dict(sql) + + if not status: + return False, internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return False, gone(self._NOT_FOUND_PUB_INFORMATION) + + get_name_sql = render_template( + "/".join([self.template_path, self._DELETE_SQL]), + pbid=pbid, conn=self.conn + ) + status, pname = self.conn.execute_scalar(get_name_sql) + table_sql = render_template( + "/".join([self.template_path, 'get_tables.sql']), + pname=pname + ) + + pub_table = [] + status, table_res = self.conn.execute_dict(table_sql) + + for table in table_res['rows']: + pub_table.append(table['pubtable']) + + res['rows'][0]['pubtable'] = pub_table + + return True, res['rows'][0] + + @check_precondition + def update(self, gid, sid, did, pbid): + """ + This function will update the data for the selected publication node. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + pbid: Publication ID + """ + data = request.form if request.form else json.loads( + request.data, encoding='utf-8' + ) + + try: + data = self._parser_data_input_from_client(data) + + sql, name = self.get_sql(data, pbid) + + # Most probably this is due to error + if not isinstance(sql, str): + return sql + sql = sql.strip('\n').strip(' ') + status, res = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res) + + return jsonify( + node=self.blueprint.generate_browser_node( + pbid, + did, + name, + icon="icon-%s" % self.node_type + ) + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def create(self, gid, sid, did): + """ + This function will create the publication object + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + """ + required_args = [ + 'name' + ] + + data = request.form if request.form else json.loads( + request.data, encoding='utf-8' + ) + for arg in required_args: + if arg not in data: + return make_json_response( + status=410, + success=0, + errormsg=gettext( + "Could not find the required parameter ({})." + ).format(arg) + ) + + try: + data = self._parser_data_input_from_client(data) + + sql = render_template("/".join([self.template_path, + self._CREATE_SQL]), + data=data, conn=self.conn) + + status, res = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res) + + sql = render_template( + "/".join([self.template_path, 'get_nodes.sql']), + conn=self.conn + ) + + status, r_set = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=r_set) + + for row in r_set['rows']: + return jsonify( + node=self.blueprint.generate_browser_node( + row['oid'], + did, + row['name'], + icon='icon-publication' + ) + ) + + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def delete(self, gid, sid, did, pbid=None): + """ + This function will drop the publication object + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + pbid: Publication ID + """ + if pbid is None: + data = request.form if request.form else json.loads( + request.data, encoding='utf-8' + ) + else: + data = {'ids': [pbid]} + + cascade = self._check_cascade_operation() + + try: + for pbid in data['ids']: + # Get name for publication from pbid + sql = render_template( + "/".join([self.template_path, self._DELETE_SQL]), + pbid=pbid, conn=self.conn + ) + status, pname = self.conn.execute_scalar(sql) + + if not status: + return internal_server_error(errormsg=pname) + + # drop publication + sql = render_template( + "/".join([self.template_path, self._DELETE_SQL]), + pname=pname, cascade=cascade, conn=self.conn + ) + + status, res = self.conn.execute_scalar(sql) + if not status: + return internal_server_error(errormsg=res) + + return make_json_response( + success=1, + info=gettext("Publication dropped") + ) + + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def msql(self, gid, sid, did, pbid=None): + """ + This function is used to return modified SQL for the selected + publication node. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + pbid: Publication ID + """ + data = {} + for k, v in request.args.items(): + try: + # comments should be taken as is because if user enters a + # json comment it is parsed by loads which should not happen + if k in ('description',): + data[k] = v + else: + data[k] = json.loads(v, encoding='utf-8') + except ValueError: + data[k] = v + try: + sql, name = self.get_sql(data, pbid) + # Most probably this is due to error + if not isinstance(sql, str): + return sql + if sql == '': + sql = "--modified SQL" + + return make_json_response( + data=sql, + status=200 + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @staticmethod + def _get_option_details(old_data, data): + """ + Return the option details + :param old_data: + :param data: + :return: data + """ + if 'evnt_insert' in data or 'evnt_delete' in data or \ + 'evnt_update' in data or 'evnt_truncate' in data: + + if 'evnt_insert' not in data: + data['evnt_insert'] = old_data['evnt_insert'] + + if 'evnt_delete' not in data: + data['evnt_delete'] = old_data['evnt_delete'] + + if 'evnt_update' not in data: + data['evnt_update'] = old_data['evnt_update'] + + if 'evnt_truncate' not in data and 'evnt_truncate' in old_data: + data['evnt_truncate'] = old_data['evnt_truncate'] + + return data + + def get_sql(self, data, pbid=None): + """ + This function will generate sql from model data. + + Args: + data: Contains the data of the selected publication node. + pbid: Publication ID + """ + required_args = [ + 'name' + ] + + if pbid is not None: + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), pbid=pbid + ) + status, res = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return gone(self._NOT_FOUND_PUB_INFORMATION) + + old_data = res['rows'][0] + for arg in required_args: + if arg not in data: + data[arg] = old_data[arg] + + # Add old event setting for future reference + data = self._get_option_details(old_data, data) + + sql = render_template( + "/".join([self.template_path, self._UPDATE_SQL]), + data=data, o_data=old_data, conn=self.conn + ) + return sql.strip('\n'), data['name'] if 'name' in data \ + else old_data['name'] + else: + + sql = render_template("/".join([self.template_path, + self._CREATE_SQL]), + data=data, conn=self.conn) + return sql.strip('\n'), data['name'] + + @check_precondition + def get_tables(self, gid, sid, did): + """ + This function returns the tables list. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + """ + res = [{'label': '', 'value': ''}] + + sql = render_template("/".join([self.template_path, + self._FUNCTIONS_SQL]), + show_sys_objects=self.blueprint. + show_system_objects, + server_type=self.manager.server_type + ) + status, rset = self.conn.execute_2darray(sql) + if not status: + return internal_server_error(errormsg=res) + for row in rset['rows']: + res.append( + { + 'label': row['table'], + 'value': row['table'] + } + ) + return make_json_response( + data=res, + status=200 + ) + + @check_precondition + def sql(self, gid, sid, did, pbid, json_resp=True): + """ + This function will generate sql to show in the sql panel for the + selected publication node. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + pbid: Publication ID + json_resp: + """ + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + pbid=pbid + ) + status, res = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return gone(self._NOT_FOUND_PUB_INFORMATION) + + # Making copy of output for future use + old_data = dict(res['rows'][0]) + + if 'all_table' in old_data and old_data['all_table']: + old_data['pubtable'] = '' + + old_data = self._parser_data_input_from_client(old_data) + + sql = render_template("/".join([self.template_path, + self._CREATE_SQL]), + data=old_data, conn=self.conn) + sql += "\n\n" + + sql_header = "-- Publication : {};".format(old_data['name']) + sql_header += "\n" + + sql_header += "-- " + + sql_header += render_template( + "/".join([self.template_path, self._DELETE_SQL]), + pname=old_data['name'], ) + + sql_header += "\n" + + sql = sql_header + sql + + if not json_resp: + return sql + + return ajax_response(response=sql) + + @check_precondition + def dependents(self, gid, sid, did, pbid): + """ + This function gets the dependents and returns an ajax response + for the publication node. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + pbid: Publication ID + """ + dependents_result = self.get_dependents(self.conn, pbid) + return ajax_response( + response=dependents_result, + status=200 + ) + + @check_precondition + def dependencies(self, gid, sid, did, pbid): + """ + This function gets the dependencies and returns an ajax response + for the publication node. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + pbid: Publication ID + """ + dependencies_result = self.get_dependencies(self.conn, pbid) + return ajax_response( + response=dependencies_result, + status=200 + ) + + +PublicationView.register_node_view(blueprint) diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/static/img/coll-publication.svg b/web/pgadmin/browser/server_groups/servers/databases/publications/static/img/coll-publication.svg new file mode 100644 index 000000000..cc946919b --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/static/img/coll-publication.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/static/img/coll-subscription.svg b/web/pgadmin/browser/server_groups/servers/databases/publications/static/img/coll-subscription.svg new file mode 100644 index 000000000..d14adfd39 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/static/img/coll-subscription.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/static/img/publication.svg b/web/pgadmin/browser/server_groups/servers/databases/publications/static/img/publication.svg new file mode 100644 index 000000000..3b582cf51 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/static/img/publication.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/static/img/subscription.svg b/web/pgadmin/browser/server_groups/servers/databases/publications/static/img/subscription.svg new file mode 100644 index 000000000..63a153c6d --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/static/img/subscription.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/static/js/publication.js b/web/pgadmin/browser/server_groups/servers/databases/publications/static/js/publication.js new file mode 100644 index 000000000..75cd821ee --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/static/js/publication.js @@ -0,0 +1,219 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2020, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +define('pgadmin.node.publication', [ + 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', + 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', + 'pgadmin.browser.collection', 'pgadmin.browser.server.privilege', +], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform) { + + // Extend the browser's collection class for publications collection + if (!pgBrowser.Nodes['coll-publication']) { + pgBrowser.Nodes['coll-publication'] = + pgBrowser.Collection.extend({ + node: 'publication', + label: gettext('Publications'), + type: 'coll-publication', + columns: ['name', 'pubowner', 'all_table'], + }); + } + + // Extend the browser's node class for publication node + if (!pgBrowser.Nodes['publication']) { + pgBrowser.Nodes['publication'] = pgBrowser.Node.extend({ + parent_type: 'database', + type: 'publication', + sqlAlterHelp: 'sql-alterpublication.html', + sqlCreateHelp: 'sql-createpublication.html', + dialogHelp: url_for('help.static', {'filename': 'publication_dialog.html'}), + label: gettext('Publication'), + hasSQL: true, + canDrop: true, + canDropCascade: true, + hasDepends: true, + Init: function() { + + // Avoid multiple registration of menus + if (this.initialized) + return; + + this.initialized = true; + + + // Add context menus for publication + pgBrowser.add_menus([{ + name: 'create_publication_on_database', node: 'database', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('Publication...'), + icon: 'wcTabIcon icon-publication', data: {action: 'create'}, + enable: pgBrowser.Nodes['database'].canCreate, + },{ + name: 'create_publication_on_coll', node: 'coll-publication', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('Publication...'), + icon: 'wcTabIcon icon-publication', data: {action: 'create'}, + },{ + name: 'create_publication', node: 'publication', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('Publication...'), + icon: 'wcTabIcon icon-publication', data: {action: 'create'}, + }]); + }, + // Define the model for publication node + model: pgBrowser.Node.Model.extend({ + idAttribute: 'oid', + defaults: { + name: undefined, + pubowner: undefined, + pubtable: undefined, + all_table: undefined, + evnt_insert:undefined, + evnt_delete:undefined, + evnt_update:undefined, + evnt_truncate:undefined, + only_table: undefined, + }, + + // Default values! + initialize: function(attrs, args) { + var isNew = (_.size(attrs) === 0); + if (isNew) { + var userInfo = pgBrowser.serverInfo[args.node_info.server._id].user; + + this.set({'pubowner': userInfo.name}, {silent: true}); + } + pgBrowser.Node.Model.prototype.initialize.apply(this, arguments); + }, + + // Define the schema for the publication node + schema: [{ + id: 'name', label: gettext('Name'), type: 'text', + mode: ['properties', 'create', 'edit'], + visible: function() { + if(!_.isUndefined(this.node_info) && !_.isUndefined(this.node_info.server) + && !_.isUndefined(this.node_info.server.version) && + this.node_info.server.version >= 100000) { + return true; + } + return false; + }, + },{ + id: 'oid', label: gettext('OID'), cell: 'string', mode: ['properties'], + type: 'text', + },{ + id: 'pubowner', label: gettext('Owner'), type: 'text', + control: Backform.NodeListByNameControl, node: 'role', + mode: ['edit', 'properties', 'create'], select2: { allowClear: false}, + },{ + id: 'all_table', label: gettext('All table?'), type: 'switch', + group: gettext('Definition'), mode: ['edit', 'properties', 'create'], deps: ['name'], + readonly: function(m) {return !m.isNew();}, + }, + { + id: 'only_table', label: gettext('Only table'), type: 'switch', + group: gettext('Definition'), mode: ['edit', 'properties', 'create'], + deps: ['name, all_table'], disabled: function(m){ + if(m.get('all_table')) + return true; + }, + }, + { + id: 'pubtable', label: gettext('Table'), type: 'array',select2: { allowClear: true, multiple: true }, + control: 'node-ajax-options', url:'get_tables', + group: gettext('Definition'), mode: ['edit', 'properties', 'create'], + deps: ['all_table'], disabled: 'isAllTable', + }, + { + type: 'nested', control: 'fieldset', mode: ['create','edit', 'properties'], + label: gettext('With'), group: gettext('Definition'), contentClass: 'row', + schema:[{ + id: 'evnt_insert', label: gettext('INSERT'), + type: 'switch', mode: ['create','edit', 'properties'], + group: gettext('With'), + 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', + },{ + id: 'evnt_update', label: gettext('UPDATE'), + type: 'switch', mode: ['create','edit', 'properties'], + group: gettext('With'), + 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', + },{ + id: 'evnt_delete', label: gettext('DELETE'), + type: 'switch', mode: ['create','edit', 'properties'], + group: gettext('With'), + 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', + },{ + id: 'evnt_truncate', label: gettext('TRUNCATE'), + type: 'switch', group: gettext('With'), + 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', + visible: function(m) { + if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server) + && !_.isUndefined(m.node_info.server.version) && + m.node_info.server.version >= 110000) + return true; + return false; + }, + + }], + }, + ], + + isAllTable: function(m){ + var all_table = m.get('all_table'); + if(all_table){ + setTimeout( function() { + m.set('pubtable', ''); + }, 10); + return true; + } + return false; + }, + /* validate function is used to validate the input given by + * the user. In case of error, message will be displayed on + * the GUI for the respective control. + */ + validate: function() { + var name = this.get('name'), + msg; + + if (_.isUndefined(name) || _.isNull(name) || + String(name).replace(/^\s+|\s+$/g, '') == '') { + msg = gettext('Name cannot be empty.'); + this.errorModel.set('name', msg); + return msg; + } else { + this.errorModel.unset('name'); + } + return null; + }, + canCreate: function(itemData, item) { + + var treeData = this.getTreeNodeHierarchy(item), + server = treeData['server']; + + // If server is less than 10 then do not allow 'create' menu + if (server && server.version < 100000) + return false; + + // by default we want to allow create menu + return true; + }, + + }), + }); + } + return pgBrowser.Nodes['coll-publication']; +}); diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/11_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/11_plus/properties.sql new file mode 100644 index 000000000..42f537238 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/11_plus/properties.sql @@ -0,0 +1,8 @@ +select c.oid as oid, c.pubname as name, +pubinsert as evnt_insert, pubupdate as evnt_update, pubdelete as evnt_delete, pubtruncate as evnt_truncate, +puballtables as all_table, +pga.rolname as pubowner from pg_publication c +join pg_authid pga on c.pubowner= pga.oid +{% if pbid %} + where c.oid = {{ pbid }} +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/create.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/create.sql new file mode 100644 index 000000000..c6d0cd896 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/create.sql @@ -0,0 +1,20 @@ +{% if data.evnt_delete or data.evnt_update or data.evnt_truncate %} +{% set add_comma_after_insert = 'insert' %} +{% endif %} +{% if data.evnt_truncate %} +{% set add_comma_after_delete = 'delete' %} +{% endif %} +{% if data.evnt_delete or data.evnt_truncate%} +{% set add_comma_after_update = 'update' %} +{% endif %} + +CREATE PUBLICATION {{ conn|qtIdent(data.name) }} +{% if data.all_table %} + FOR ALL TABLES +{% elif data.pubtable %} + FOR TABLE {% if data.only_table%}ONLY {% endif %} {% for table in data.pubtable %}{% if loop.index != 1 %},{% endif %}{{ table }}{% endfor %} +{% endif %} + +{% if data.evnt_insert or data.evnt_update or data.evnt_delete or data.evnt_truncate %} +with (publish = '{% if data.evnt_insert %}insert{% if add_comma_after_insert == 'insert' %},{% endif %}{% endif %}{% if data.evnt_update %}update{% if add_comma_after_update == 'update' %},{% endif %}{% endif %}{% if data.evnt_delete %}delete{% if add_comma_after_delete == 'delete' %},{% endif %}{% endif %}{% if data.evnt_truncate %}truncate {% endif %}'); +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/delete.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/delete.sql new file mode 100644 index 000000000..98cb5d0d1 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/delete.sql @@ -0,0 +1,8 @@ +{# ============= Get the publication name using oid ============= #} +{% if pbid %} + SELECT pubname FROM pg_publication WHERE oid = {{pbid}}::oid; +{% endif %} +{# ============= Drop the publication ============= #} +{% if pname %} +DROP PUBLICATION {{ conn|qtIdent(pname) }}{% if cascade %} CASCADE{% endif%}; +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/functions.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/functions.sql new file mode 100644 index 000000000..b493abc7b --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/functions.sql @@ -0,0 +1,6 @@ +SELECT quote_ident(c.table_schema)||'.'||quote_ident(c.table_name) AS table +from information_schema.tables c +where c.table_type = 'BASE TABLE' + AND c.table_schema NOT LIKE 'pg\_%' + AND c.table_schema NOT LIKE 'pgagent' + AND c.table_schema NOT IN ('information_schema') ORDER BY 1; diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/get_nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/get_nodes.sql new file mode 100644 index 000000000..8b2321b2d --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/get_nodes.sql @@ -0,0 +1 @@ +select oid, pubname as name from pg_publication; diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/get_tables.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/get_tables.sql new file mode 100644 index 000000000..7f51c72d1 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/get_tables.sql @@ -0,0 +1 @@ +select quote_ident(pgb_table.schemaname)||'.'||quote_ident(pgb_table.tablename) as pubtable from pg_publication_tables pgb_table where pubname = '{{ pname }}' and pgb_table.schemaname NOT LIKE 'pgagent'; diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/nodes.sql new file mode 100644 index 000000000..c8157f180 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/nodes.sql @@ -0,0 +1 @@ +select oid , pubname as name from pg_publication; diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/properties.sql new file mode 100644 index 000000000..75ba075e4 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/properties.sql @@ -0,0 +1,8 @@ +select c.oid as oid, c.pubname as name, +pubinsert as evnt_insert, pubupdate as evnt_update, pubdelete as evnt_delete, +puballtables as all_table, +pga.rolname as pubowner from pg_publication c +join pg_authid pga on c.pubowner= pga.oid +{% if pbid %} + where c.oid = {{ pbid }} +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/update.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/update.sql new file mode 100644 index 000000000..dd348c042 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/templates/publications/sql/default/update.sql @@ -0,0 +1,39 @@ +{% if data.evnt_delete or data.evnt_update or data.evnt_truncate %} +{% set add_comma_after_insert = 'insert' %} +{% endif %} +{% if data.evnt_truncate %} +{% set add_comma_after_delete = 'delete' %} +{% endif %} +{% if data.evnt_delete or data.evnt_truncate%} +{% set add_comma_after_update = 'update' %} +{% endif %} + +{% if data.pubowner %} +ALTER PUBLICATION {{ conn|qtIdent(o_data.name) }} OWNER TO {{ data.pubowner }}; +{% endif %} + +{% if data.evnt_insert or data.evnt_update or data.evnt_delete %} +ALTER PUBLICATION {{ conn|qtIdent(o_data.name) }} SET +(publish = '{% if data.evnt_insert %}insert{% if add_comma_after_insert == 'insert' %},{% endif %}{% endif %}{% if data.evnt_update %}update{% if add_comma_after_update == 'update' %},{% endif %}{% endif %}{% if data.evnt_delete %}delete{% if add_comma_after_delete == 'delete' %},{% endif %}{% endif %}{% if data.evnt_truncate %}truncate {% endif %}'); +{% endif %} + +{% if data.pubtable %} +ALTER PUBLICATION {{ conn|qtIdent(o_data.name) }} SET TABLE {% if data.only_table%}ONLY {% endif %}{% for table in data.pubtable %}{% if loop.index != 1 %},{% endif %}{{ table }}{% endfor %}; +{% endif %} + +{% if data.pubtable and data.sometable%} +{% if data.pubtable and o_data.pubtable not in data.pubtable %} +ALTER PUBLICATION conn|qtIdent(o_data.name) SET TABLE {% for table in data.pubtable %} {% if loop.index != 1 %}, {% endif %} {{ table }} {% endfor %} +{% elif data.pubtable and data.pubtable not in o_data.pubtable %} +ALTER PUBLICATION {{ conn|qtIdent(o_data.name) }} ADD TABLE {% for table in data.pubtable %} {% if loop.index != 1 %}, {% endif %} {{ table }} {% endfor %} +{{ data.pubtable }} +{% endif %} +{% endif %} + +{% if data.name != o_data.name %} +ALTER PUBLICATION {{ conn|qtIdent(o_data.name) }} RENAME TO {{ conn|qtIdent(data.name) }}; +{% endif %} + + + + diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/alter_publication.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/alter_publication.sql new file mode 100644 index 000000000..f6298f2ae --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/alter_publication.sql @@ -0,0 +1,8 @@ +-- Publication : alterd_publication; +-- DROP PUBLICATION alterd_publication; + + +CREATE PUBLICATION alterd_publication + FOR ALL TABLES + +with (publish = 'insert,update'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/alter_publication_event.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/alter_publication_event.sql new file mode 100644 index 000000000..f6298f2ae --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/alter_publication_event.sql @@ -0,0 +1,8 @@ +-- Publication : alterd_publication; +-- DROP PUBLICATION alterd_publication; + + +CREATE PUBLICATION alterd_publication + FOR ALL TABLES + +with (publish = 'insert,update'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/alter_publication_event_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/alter_publication_event_msql.sql new file mode 100644 index 000000000..e2a94423e --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/alter_publication_event_msql.sql @@ -0,0 +1,2 @@ +ALTER PUBLICATION alterd_publication_event SET +(publish = 'insert,update'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/alter_publication_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/alter_publication_msql.sql new file mode 100644 index 000000000..30869d2a8 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/alter_publication_msql.sql @@ -0,0 +1 @@ +ALTER PUBLICATION test_publication_to_alter RENAME TO alterd_publication; diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/create_publication.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/create_publication.sql new file mode 100644 index 000000000..aa0cb1590 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/create_publication.sql @@ -0,0 +1,8 @@ +-- Publication : test_publication_create; +-- DROP PUBLICATION test_publication_create; + + +CREATE PUBLICATION test_publication_create + FOR ALL TABLES + +with (publish = 'insert,update'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/create_publication_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/create_publication_msql.sql new file mode 100644 index 000000000..d071eba54 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/create_publication_msql.sql @@ -0,0 +1,4 @@ +CREATE PUBLICATION test_publication_create + FOR ALL TABLES + +with (publish = 'insert,update'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/create_publication_update.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/create_publication_update.sql new file mode 100644 index 000000000..e4ae78ff8 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/create_publication_update.sql @@ -0,0 +1,8 @@ +-- Publication : test_publication_with_update; +-- DROP PUBLICATION test_publication_with_update; + + +CREATE PUBLICATION test_publication_with_update + FOR ALL TABLES + +with (publish = 'update'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/create_publication_update_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/create_publication_update_msql.sql new file mode 100644 index 000000000..33abf9457 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/create_publication_update_msql.sql @@ -0,0 +1,4 @@ +CREATE PUBLICATION test_publication_with_update + FOR ALL TABLES + +with (publish = 'update'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/test.json b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/test.json new file mode 100644 index 000000000..99d605783 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/10_plus/test.json @@ -0,0 +1,79 @@ +{ + "scenarios": [ + { + "type": "create", + "name": "Create Table For publication", + "endpoint": "NODE-table.obj", + "sql_endpoint": "NODE-table.sql_id", + "data": { + "name": "test_publication", + "columns": [ + { + "name": "emp_id", + "cltype": "integer", + "is_primary_key": true + }, + { + "name": "name", + "cltype": "text" + }, + { + "name": "salary", + "cltype": "bigint" + } + ], + "is_partitioned": false, + "schema": "public", + "spcname": "pg_default" + }, + "store_object_id": true + }, + { + "type": "create", + "name": "Create Publication", + "endpoint": "NODE-publication.obj", + "sql_endpoint": "NODE-publication.sql_id", + "msql_endpoint": "NODE-publication.msql", + "data": { + "name": "test_publication_create", + "evnt_insert": true, + "evnt_update": true, + "evnt_delete": false, + "evnt_truncate": false, + "pubowner": "postgres", + "all_table": true, + "pubtable": "" + }, + "expected_sql_file": "create_publication.sql", + "expected_msql_file": "create_publication_msql.sql" + }, + { + "type": "alter", + "name": "Alter Publication name", + "endpoint": "NODE-publication.obj_id", + "sql_endpoint": "NODE-publication.sql_id", + "data": { + "name": "alterd_publication" + }, + "expected_sql_file": "alter_publication.sql" + }, + { + "type": "alter", + "name": "Alter Publication event", + "endpoint": "NODE-publication.obj_id", + "sql_endpoint": "NODE-publication.sql_id", + "data": { + "evnt_insert": true + }, + "expected_sql_file": "alter_publication_event.sql" + }, + { + "type": "delete", + "name": "Drop publication", + "endpoint": "NODE-publication.delete_id", + "data": { + "name": "alterd_publication_event" + } + } + ] +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/__init__.py new file mode 100644 index 000000000..27cd89067 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/__init__.py @@ -0,0 +1,16 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from pgadmin.utils.route import BaseTestGenerator + + +class RulesTestGenerator(BaseTestGenerator): + + def runTest(self): + return [] diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/publication_test_data.json b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/publication_test_data.json new file mode 100644 index 000000000..da08408db --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/publication_test_data.json @@ -0,0 +1,397 @@ +{ + "add_publication": [ + { + "name": "Create publication with insert and update", + "url": "/browser/publication/obj/", + "is_positive_test": true, + "test_data": { + "name": "PLACEHOLDER", + "evnt_insert": true, + "evnt_update": true, + "evnt_delete": false, + "evnt_truncate": false, + "pubowner": "postgres", + "all_table": true, + "pubtable": "" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Create publication for few tables", + "url": "/browser/publication/obj/", + "is_positive_test": true, + "few_tables": true, + "test_data": { + "name": "PLACEHOLDER", + "evnt_insert": true, + "evnt_update": true, + "evnt_delete": false, + "evnt_truncate": false, + "pubowner": "postgres", + "all_table": false, + "pubtable": "PLACE_HOLDER" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Create a publication without name", + "url": "/browser/publication/obj/", + "is_positive_test": false, + "without_name": true, + "test_data": { + "name": "PLACEHOLDER", + "evnt_insert": true, + "evnt_update": true, + "evnt_delete": false, + "evnt_truncate": false, + "pubowner": "postgres", + "all_table": true, + "pubtable": "" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 410 + } + }, + { + "name": "Error while adding a publication", + "url": "/browser/publication/obj/", + "is_positive_test": false, + "error_creating_publication": true, + "test_data": { + "name": "PLACEHOLDER", + "evnt_insert": true, + "evnt_update": true, + "evnt_delete": false, + "evnt_truncate": false, + "pubowner": "postgres", + "all_table": true, + "pubtable": "" + }, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(False, 'Mocked Internal Server Error ')" + }, + "expected_data": { + "status_code": 500 + } + }, + { + "name": "Exception while adding a publication", + "url": "/browser/publication/obj/", + "is_positive_test": false, + "test_data": { + "name": "PLACEHOLDER", + "evnt_insert": true, + "evnt_update": true, + "evnt_delete": false, + "evnt_truncate": false, + "pubowner": "postgres", + "all_table": true, + "pubtable": "" + }, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(True, True)(False, 'Mocked Internal Server Error ')" + }, + "expected_data": { + "status_code": 500 + } + } + ], + "get_publication": [ + { + "name": "Get a publication URL", + "url": "/browser/publication/obj/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while fetching a publication properties", + "url": "/browser/publication/obj/", + "is_positive_test": false, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + }, + { + "name": "Get a publication properties under database nodes", + "url": "/browser/publication/obj/", + "is_positive_test": true, + "database_nodes": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while fetching a publication properties under database nodes", + "url": "/browser/publication/obj/", + "is_positive_test": false, + "database_nodes": true, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + }, + { + "name": "Get a publication Node", + "url": "/browser/publication/nodes/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get a publication Node dependants", + "url": "/browser/publication/dependent/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get a publication Node dependency", + "url": "/browser/publication/dependency/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while fetching the publication under the database nodes using wrong database id", + "url": "/browser/publication/nodes/", + "is_positive_test": false, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_2darray", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + }, + { + "name": "Get all the publication under the database nodes", + "url": "/browser/publication/nodes/", + "is_positive_test": true, + "mocking_required": false, + "database_nodes": true, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get all the publication under the database nodes using wrong database id", + "url": "/browser/publication/nodes/", + "is_positive_test": true, + "incorrect_database_id": true, + "database_nodes": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while fetching all the publication under the database nodes using wrong database id", + "url": "/browser/publication/nodes/", + "is_positive_test": false, + "database_nodes": true, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_2darray", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + } + ], + "delete_publication": [ + { + "name": "Delete a publication URL", + "url": "/browser/publication/obj/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while deleting the publication", + "url": "/browser/publication/obj/", + "is_positive_test": false, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_scalar", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + } + ], + "update_publication": [ + { + "name": "update a publication name", + "url": "/browser/publication/obj/", + "is_positive_test": true, + "update_name": true, + "test_data": { + "name": "PLACE_HOLDER", + "id": "PLACE_HOLDER" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "update a publication event insert", + "url": "/browser/publication/obj/", + "is_positive_test": true, + "owner_publication": true, + "test_data": { + "id": "PLACE_HOLDER", + "evnt_insert": "PLACEHOLDER" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "update a publication event delete", + "url": "/browser/publication/obj/", + "is_positive_test": true, + "owner_publication": true, + "test_data": { + "id": "PLACE_HOLDER", + "evnt_delete": "PLACEHOLDER" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while fetching a publication to update", + "url": "/browser/publication/obj/", + "is_positive_test": false, + "mocking_required": true, + "test_data": { + "name": "PLACE_HOLDER", + "id": "PLACE_HOLDER" + }, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + }, + { + "name": "Error while fetching a publication to update using wrong publication id", + "url": "/browser/publication/obj/", + "is_positive_test": true, + "wrong_publication_id": true, + "mocking_required": false, + "test_data": { + "id": "PLACE_HOLDER" + }, + "mock_data": {}, + "expected_data": { + "status_code": 500 + } + } + ], + "delete_multiple_publication": [ + { + "name": "Delete multiple publication", + "url": "/browser/publication/obj/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + } + ], + "sql_publication": [ + { + "name": "Fetch the publication SQL", + "url": "/browser/publication/sql/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": { + }, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while fetching a publication SQL", + "url": "/browser/publication/sql/", + "is_positive_test": false, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + }, + { + "name": "Fetching a publication mSQL", + "url": "/browser/publication/msql/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 200 + } + } + ] +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_create.py b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_create.py new file mode 100644 index 000000000..aad3b33d2 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_create.py @@ -0,0 +1,95 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import json +import uuid +from unittest.mock import patch + +from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \ + import utils as tables_utils +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 . import utils as publication_utils + + +class PublicationsAddTestCase(BaseTestGenerator): + """This class will add new publication""" + scenarios = utils.generate_scenarios('add_publication', + publication_utils.test_cases) + + def setUp(self): + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + self.schema_name = schema_info["schema_name"] + + self.server_version = schema_info["server_version"] + if self.server_version < 99999: + self.skipTest( + "Logical replication is not supported " + "for server version less than 10" + + ) + + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception( + "Could not connect to database to add a publication.") + + if self.is_positive_test and hasattr(self, 'few_tables'): + self.table_name = "table_column_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = tables_utils. \ + create_table(self.server, self.db_name, self.schema_name, + self.table_name) + + self.test_data['pubtable'] = publication_utils.get_tables(self) + + def runTest(self): + """This function will publication.""" + self.test_data['name'] = \ + "test_publication_add_%s" % (str(uuid.uuid4())[1:8]) + + data = self.test_data + if self.is_positive_test: + response = self.create_publication() + else: + if hasattr(self, 'without_name'): + del data["name"] + response = self.create_publication() + elif hasattr(self, 'error_creating_publication'): + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + response = self.create_publication() + else: + with patch(self.mock_data["function_name"], + side_effect=self.mock_data["return_value"]): + response = self.create_publication() + self.assertEqual(response.status_code, + self.expected_data["status_code"]) + + def create_publication(self): + return self.tester.post( + self.url + str(utils.SERVER_GROUP) + '/' + + str(self.server_id) + '/' + str( + self.db_id) + '/', + data=json.dumps(self.test_data), + content_type='html/json') + + def tearDown(self): + if not hasattr(self, 'without_name'): + publication_utils.delete_publication(self.server, self.db_name, + self.test_data['name']) + + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_delete.py b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_delete.py new file mode 100644 index 000000000..8839bd23f --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_delete.py @@ -0,0 +1,94 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +from unittest.mock import patch +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +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 . import utils as publication_utils + + +class PublicationDeleteTestCase(BaseTestGenerator): + """This class will delete publication.""" + scenarios = utils.generate_scenarios('delete_publication', + publication_utils.test_cases) + + def setUp(self): + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + self.server_version = schema_info["server_version"] + if self.server_version < 99999: + self.skipTest( + "Logical replication is not supported " + "for server version less than 10" + + ) + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception( + "Could not connect to database to delete publication.") + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to delete publication.") + self.publication_name = "test_publication_delete_%s" % ( + str(uuid.uuid4())[1:8]) + + self.publication_id = \ + publication_utils.create_publication(self.server, + self.db_name, + self.publication_name) + + def delete_publication(self): + return self.tester.delete( + self.url + str(utils.SERVER_GROUP) + '/' + + str(self.server_id) + '/' + str(self.db_id) + + '/' + str(self.publication_id), + follow_redirects=True) + + def runTest(self): + """This function will delete publication""" + publication_response = publication_utils. \ + verify_publication(self.server, + self.db_name, + self.publication_name) + if not publication_response: + raise Exception("Could not find the publication to delete.") + + if self.is_positive_test: + if hasattr(self, "invalid_publication_id"): + self.publication_id = 9999 + response = self.delete_publication() + else: + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + response = self.delete_publication() + + self.assertEqual(response.status_code, + self.expected_data["status_code"]) + + def tearDown(self): + # Disconnect the database + if not self.is_positive_test or hasattr(self, + 'invalid_publication_id'): + publication_utils.delete_publication(self.server, self.db_name, + self.publication_name) + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_delete_multiple.py b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_delete_multiple.py new file mode 100644 index 000000000..a96812b24 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_delete_multiple.py @@ -0,0 +1,103 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +import json + +from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \ + import utils as tables_utils +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +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 . import utils as publication_utils + + +class PublicationDeleteTestCases(BaseTestGenerator): + """This class will delete publication.""" + + scenarios = utils.generate_scenarios('delete_multiple_publication', + publication_utils.test_cases) + + def setUp(self): + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception( + "Could not connect to database to delete publication.") + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + self.server_version = schema_info["server_version"] + if self.server_version < 99999: + self.skipTest( + "Logical replication is not supported " + "for server version less than 10" + + ) + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to delete publication.") + self.table_name = "table_column_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = tables_utils.create_table(self.server, self.db_name, + self.schema_name, + self.table_name) + self.publication_name = "test_publication_delete_%s" % ( + str(uuid.uuid4())[1:8]) + self.publication_name_1 = "test_publication_delete_%s" % ( + str(uuid.uuid4())[1:8]) + self.publication_ids = [ + publication_utils.create_publication(self.server, self.db_name, + self.publication_name), + publication_utils.create_publication(self.server, self.db_name, + self.publication_name_1), + ] + + def delete_multiple_publication(self, data): + return self.tester.delete( + "{0}{1}/{2}/{3}/".format(self.url, utils.SERVER_GROUP, + self.server_id, self.db_id + ), + follow_redirects=True, + data=json.dumps(data), + content_type='html/json' + ) + + def runTest(self): + """This function will delete publication.""" + rule_response = publication_utils. \ + verify_publication(self.server, + self.db_name, + self.publication_name) + if not rule_response: + raise Exception("Could not find the publication to delete.") + + rule_response = publication_utils. \ + verify_publication(self.server, + self.db_name, + self.publication_name_1) + if not rule_response: + raise Exception("Could not find the publication to delete.") + + data = {'ids': self.publication_ids} + response = self.delete_multiple_publication(data) + self.assertEqual(response.status_code, + self.expected_data["status_code"]) + + def tearDown(self): + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_get.py b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_get.py new file mode 100644 index 000000000..cba8794cf --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_get.py @@ -0,0 +1,100 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +from unittest.mock import patch + +from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \ + import utils as tables_utils +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +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 . import utils as publication_utils + + +class PublicationGetTestCase(BaseTestGenerator): + """This class will fetch the publication under table node.""" + scenarios = utils.generate_scenarios('get_publication', + publication_utils.test_cases) + + def setUp(self): + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + self.server_version = schema_info["server_version"] + if self.server_version < 99999: + self.skipTest( + "Logical replication is not supported " + "for server version less than 10" + + ) + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception( + "Could not connect to database to delete publication.") + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + + if not schema_response: + raise Exception("Could not find the schema to delete publication.") + self.table_name = "table_column_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = tables_utils.create_table(self.server, self.db_name, + self.schema_name, + self.table_name) + self.publication_name = "test_publication_get_%s" % ( + str(uuid.uuid4())[1:8]) + self.publication_id = publication_utils. \ + create_publication(self.server, + self.db_name, + self.publication_name) + + def get_publication(self): + return self.tester.get( + self.url + str(utils.SERVER_GROUP) + '/' + str( + self.server_id) + '/' + + str(self.db_id) + '/' + str(self.publication_id), + content_type='html/json') + + def runTest(self): + """This function will fetch the publication.""" + + if self.is_positive_test: + if hasattr(self, "database_nodes"): + self.publication_id = '' + response = self.get_publication() + else: + response = self.get_publication() + else: + if hasattr(self, "database_nodes"): + self.publication_id = '' + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + response = self.get_publication() + else: + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + response = self.get_publication() + + self.assertEqual(response.status_code, + self.expected_data["status_code"]) + + def tearDown(self): + publication_utils.delete_publication(self.server, self.db_name, + self.publication_name) + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_put.py b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_put.py new file mode 100644 index 000000000..bc31602ab --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_put.py @@ -0,0 +1,127 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import json +import uuid +from unittest.mock import patch + +from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \ + import utils as tables_utils +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +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 . import utils as publication_utils + + +class PublicationUpdateTestCase(BaseTestGenerator): + """This class will update the publication.""" + scenarios = utils.generate_scenarios('update_publication', + publication_utils.test_cases) + + def setUp(self): + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + self.server_version = schema_info["server_version"] + if self.server_version < 99999: + self.skipTest( + "Logical replication is not supported " + "for server version less than 10" + + ) + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception( + "Could not connect to database to delete publication.") + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to delete publication.") + self.table_name = "table_column_%s" % (str(uuid.uuid4())[1:8]) + + self.server_version = schema_info["server_version"] + if self.server_version < 99999: + self.skipTest( + "Logical replication is not supported " + "for server version less than 10" + + ) + self.table_id = tables_utils.create_table(self.server, self.db_name, + self.schema_name, + self.table_name) + self.publication_name = "test_publication_update_%s" % ( + str(uuid.uuid4())[1:8]) + self.publication_id = \ + publication_utils.create_publication(self.server, self.db_name, + self.publication_name) + + def update_publication(self, data): + return self.tester.put( + self.url + str(utils.SERVER_GROUP) + '/' + + str(self.server_id) + '/' + str( + self.db_id) + + '/' + str(self.publication_id), + data=json.dumps(data), + follow_redirects=True) + + def runTest(self): + """This function will update the publication.""" + + publication_name = publication_utils. \ + verify_publication(self.server, + self.db_name, + self.publication_name) + if hasattr(self, "update_name"): + self.test_data['name'] = "test_publication_update_%s" % ( + str(uuid.uuid4())[1:8]) + else: + self.test_data['name'] = self.publication_name + self.test_data['id'] = self.publication_id + + if not publication_name: + raise Exception("Could not find the publication to update.") + + if self.is_positive_test: + if hasattr(self, "wrong_publication_id"): + self.publication_id = 9999 + if hasattr(self, "plid_none"): + self.publication_id = '' + response = self.update_publication(self.test_data) + else: + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + if hasattr(self, "wrong_publication_id"): + self.publication_id = 9999 + response = self.update_publication(self.test_data) + + self.assertEqual(response.status_code, + self.expected_data["status_code"]) + + def tearDown(self): + connection = utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port'], + self.server['sslmode']) + + publication_utils.delete_publication(self.server, self.db_name, + self.test_data['name']) + + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_sql.py b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_sql.py new file mode 100644 index 000000000..b1acb7874 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/test_publication_sql.py @@ -0,0 +1,88 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +from unittest.mock import patch +import json +from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \ + import utils as tables_utils +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +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 . import utils as publication_utils + + +class PublicationGetTestCase(BaseTestGenerator): + """This class will fetch the publication under table node.""" + scenarios = utils.generate_scenarios('sql_publication', + publication_utils.test_cases) + + def setUp(self): + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + self.server_version = schema_info["server_version"] + if self.server_version < 99999: + self.skipTest( + "Logical replication is not supported " + "for server version less than 10" + + ) + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception( + "Could not connect to database to delete publication.") + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to delete publication.") + self.table_name = "table_column_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = tables_utils.create_table(self.server, self.db_name, + self.schema_name, + self.table_name) + self.publication_name = "test_publication_delete_%s" % ( + str(uuid.uuid4())[1:8]) + self.publication_id = \ + publication_utils.create_publication(self.server, self.db_name, + self.publication_name) + + def get_sql(self): + return self.tester.get( + self.url + str(utils.SERVER_GROUP) + '/' + str( + self.server_id) + '/' + + str(self.db_id) + '/' + str(self.publication_id), + content_type='html/json') + + def runTest(self): + """This function will fetch the publication under table node.""" + + if self.is_positive_test: + response = self.get_sql() + else: + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + response = self.get_sql() + + self.assertEqual(response.status_code, + self.expected_data["status_code"]) + + def tearDown(self): + publication_utils.delete_publication(self.server, self.db_name, + self.publication_name) + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/tests/utils.py b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/utils.py new file mode 100644 index 000000000..be399a4c6 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/tests/utils.py @@ -0,0 +1,152 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + + +import sys +import os +import json +import traceback + +from regression.python_test_utils import test_utils as utils + +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/publication_test_data.json") as data_file: + test_cases = json.load(data_file) + + +def get_tables(self): + tables = self.tester.get( + '/browser/publication/get_tables/' + str( + utils.SERVER_GROUP) + '/' + str( + self.server_id) + '/' + + str(self.db_id) + '/', + content_type='html/json') + return json.dumps([tables.json['data'][1]['value']]) + + +def create_publication_api(self): + return self.tester.post( + self.url + str(utils.SERVER_GROUP) + '/' + + str(self.server_id) + '/' + str( + self.db_id) + '/', + data=json.dumps(self.test_data), + content_type='html/json') + + +def create_publication(server, db_name, publication_name): + """ + This function creates a publication under provided table. + :param server: server details + :type server: dict + :param db_name: database name + :type db_name: str + :param publication_name: publication name + :type publication_name: str + :return publication_id: publication id + :rtype: int + """ + try: + connection = utils.get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode']) + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + query = "CREATE publication %s FOR ALL TABLES" % \ + (publication_name) + pg_cursor.execute(query) + connection.set_isolation_level(old_isolation_level) + connection.commit() + # Get role oid of newly added publication + pg_cursor.execute("select oid from pg_publication pub where " + "pub.pubname='%s'" % + publication_name) + publication = pg_cursor.fetchone() + publication_id = '' + if publication: + publication_id = publication[0] + connection.close() + return publication_id + except Exception: + traceback.print_exc(file=sys.stderr) + raise + + +def verify_publication(server, db_name, publication_name): + """ + This function verifies publication exist in database or not. + :param server: server details + :type server: dict + :param db_name: database name + :type db_name: str + :param publication_name: publication name + :type publication_name: str + :return publication: publication record from database + :rtype: tuple + """ + try: + connection = utils.get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode']) + pg_cursor = connection.cursor() + pg_cursor.execute("select * from pg_publication pub " + "where pub.pubname='%s'" % + publication_name) + publication = pg_cursor.fetchone() + connection.close() + return publication + except Exception: + traceback.print_exc(file=sys.stderr) + raise + + +def delete_publication(server, db_name, publication_name): + """ + This function use to delete the existing roles in the servers + + :param db_name: db_name + :type db_name: db_name object + :param server: server + :type server: server object + :param publication_name: publication name + :type publication_name: str + :return: None + """ + + try: + connection = utils.get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode']) + pg_cursor = connection.cursor() + + pg_cursor.execute("select * from pg_publication pub where " + "pub.pubname='%s'" % + publication_name) + publication_count = pg_cursor.fetchone() + if publication_count: + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + query = "DROP publication %s" % publication_name + pg_cursor.execute(query) + connection.set_isolation_level(old_isolation_level) + connection.commit() + connection.close() + except Exception: + traceback.print_exc(file=sys.stderr) + raise diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js index 6a97dd5ba..76b1a09d8 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js +++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js @@ -95,6 +95,18 @@ define('pgadmin.node.database', [ return server.connected && server.user.can_create_db; }, + canCreate: function(itemData, item) { + var treeData = this.getTreeNodeHierarchy(item), + server = treeData['server']; + + // If server is less than 10 then do not allow 'create' menu + if (server && server.version < 100000) + return false; + + // by default we want to allow create menu + return true; + }, + is_not_connected: function(node) { return (node && node.connected != true && node.allowConn == true); }, diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/__init__.py new file mode 100644 index 000000000..2e75a1551 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/__init__.py @@ -0,0 +1,792 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Implements Subscription Node""" + +import simplejson as json +from functools import wraps + +import pgadmin.browser.server_groups.servers.databases as databases +from flask import render_template, request, jsonify +from flask_babelex import gettext +from pgadmin.browser.collection import CollectionNodeModule +from pgadmin.browser.utils import PGChildNodeView +from pgadmin.utils.ajax import make_json_response, internal_server_error, \ + make_response as ajax_response, gone +from pgadmin.utils.driver import get_driver +from config import PG_DEFAULT_DRIVER +from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry +from pgadmin.tools.schema_diff.compare import SchemaDiffObjectCompare +import psycopg2 +from pgadmin.utils import get_complete_file_path + + +class SubscriptionModule(CollectionNodeModule): + """ + class SubscriptionModule(CollectionNodeModule) + + A module class for Subscription node derived from CollectionNodeModule. + + Methods: + ------- + * __init__(*args, **kwargs) + - Method is used to initialize the SubscriptionModule and it's + base module. + + * get_nodes(gid, sid, did) + - Method is used to generate the browser collection node. + + * node_inode() + - Method is overridden from its base class to make the node as leaf node. + + * script_load() + - Load the module script for subscription, when any of the database node + is initialized. + """ + + _NODE_TYPE = 'subscription' + _COLLECTION_LABEL = gettext("Subscriptions") + + def __init__(self, *args, **kwargs): + """ + Method is used to initialize the SubscriptionModule and it's + base module. + + Args: + *args: + **kwargs: + """ + super(SubscriptionModule, self).__init__(*args, **kwargs) + self.min_ver = 100000 + self.max_ver = None + + def get_nodes(self, gid, sid, did): + """ + Method is used to generate the browser collection node + + Args: + gid: Server Group ID + sid: Server ID + did: Database Id + """ + yield self.generate_browser_collection_node(did) + + @property + def node_inode(self): + """ + Override this property to make the node a leaf node. + + Returns: False as this is the leaf node + """ + return False + + @property + def script_load(self): + """ + Load the module script for subscription, when any of the database nodes + are initialized. + + Returns: node type of the server module. + """ + return databases.DatabaseModule.node_type + + @property + def module_use_template_javascript(self): + """ + Returns whether Jinja2 template is used for generating the javascript + module. + """ + return False + + +blueprint = SubscriptionModule(__name__) + + +class SubscriptionView(PGChildNodeView, SchemaDiffObjectCompare): + """ + class SubscriptionView(PGChildNodeView) + + A view class for Subscription node derived from PGChildNodeView. + This class is responsible for all the stuff related to view like + updating subscription node, showing properties, showing sql + in sql pane. + + Methods: + ------- + * __init__(**kwargs) + - Method is used to initialize the SubscriptionView and it's base view. + + * check_precondition() + - This function will behave as a decorator which will checks + database connection before running view, it will also attaches + manager,conn & template_path properties to self + + * list() + - This function is used to list all the subscription nodes within that + collection. + + * nodes() + - This function will used to create all the child node within that + collection. Here it will create all the subscription node. + + * properties(gid, sid, did, subid) + - This function will show the properties of the selected + subscription node + + * update(gid, sid, did, subid) + - This function will update the data for the selected subscription node + + * create(gid, sid, did) + - This function will create the new subscription node + + * delete(gid, sid, did, subid) + - This function will delete the selected subscription node + + * msql(gid, sid, did, subid) + - This function is used to return modified SQL for the selected + subscription node + + * get_sql(data, subid) + - This function will generate sql from model data + + * get_publications(gid, sid, did) + - This function returns the publications list + + * get_templates(gid, sid, did) + - This function returns subscription templates. + + * sql(gid, sid, did, subid): + - This function will generate sql to show it in sql pane for the + selected subscription node. + + * dependents(gid, sid, did, subid): + - This function get the dependents and return ajax response for the + subscription node. + + * dependencies(self, gid, sid, did, subid): + - This function get the dependencies and return ajax response for the + subscription node. + """ + + _NOT_FOUND_PUB_INFORMATION = \ + gettext("Could not find the subscription information.") + node_type = blueprint.node_type + + parent_ids = [ + {'type': 'int', 'id': 'gid'}, + {'type': 'int', 'id': 'sid'}, + {'type': 'int', 'id': 'did'} + ] + ids = [ + {'type': 'int', 'id': 'subid'} + ] + + operations = dict({ + 'obj': [ + {'get': 'properties', 'delete': 'delete', 'put': 'update'}, + {'get': 'list', 'post': 'create', 'delete': 'delete'} + ], + 'nodes': [{'get': 'node'}, {'get': 'nodes'}], + 'sql': [{'get': 'sql'}], + 'msql': [{'get': 'msql'}, {'get': 'msql'}], + 'stats': [{'get': 'statistics'}], + 'dependency': [{'get': 'dependencies'}], + 'dependent': [{'get': 'dependents'}], + 'get_publications': [{}, {'get': 'get_publications'}], + 'get_templates': [{}, {'get': 'get_templates'}], + 'delete': [{'delete': 'delete'}, {'delete': 'delete'}] + }) + + def _init_(self, **kwargs): + """ + Method is used to initialize the SubscriptionView and its base view. + Initialize all the variables create/used dynamically like conn, + template_path. + + Args: + **kwargs: + """ + self.conn = None + self.template_path = None + self.manager = None + + super(SubscriptionView, self).__init__(**kwargs) + + def check_precondition(f): + """ + This function will behave as a decorator which will check the + database connection before running the view. It also attaches + manager, conn & template_path properties to self + """ + + @wraps(f) + def wrap(*args, **kwargs): + # Here args[0] will hold self & kwargs will hold gid,sid,did + self = args[0] + self.driver = get_driver(PG_DEFAULT_DRIVER) + self.manager = self.driver.connection_manager(kwargs['sid']) + self.conn = self.manager.connection(did=kwargs['did']) + self.datlastsysoid = self.manager.db_info[kwargs['did']][ + 'datlastsysoid'] if self.manager.db_info is not None \ + and kwargs['did'] in self.manager.db_info else 0 + + # Set the template path for the SQL scripts + self.template_path = ( + "subscriptions/sql/#gpdb#{0}#".format(self.manager.version) if + self.manager.server_type == 'gpdb' else + "subscriptions/sql/#{0}#".format(self.manager.version) + ) + + return f(*args, **kwargs) + + return wrap + + @check_precondition + def list(self, gid, sid, did): + """ + This function is used to list all the subscription nodes within that + collection. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + """ + sql = render_template("/".join([self.template_path, + self._PROPERTIES_SQL]), did=did) + status, res = self.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=res) + return ajax_response( + response=res['rows'], + status=200 + ) + + @check_precondition + def nodes(self, gid, sid, did): + """ + This function is used to create all the child nodes within the + collection. Here it will create all the subscription nodes. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + """ + res = [] + sql = render_template("/".join([self.template_path, + 'nodes.sql']), did=did) + status, result = self.conn.execute_2darray(sql) + if not status: + return internal_server_error(errormsg=result) + + for row in result['rows']: + res.append( + self.blueprint.generate_browser_node( + row['oid'], + did, + row['name'], + icon="icon-subscription" + )) + + return make_json_response( + data=res, + status=200 + ) + + @check_precondition + def node(self, gid, sid, did, subid): + """ + This function will fetch properties of the subscription nodes. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + subid: Subscription ID + """ + sql = render_template("/".join([self.template_path, + self._PROPERTIES_SQL]), + subid=subid) + status, result = self.conn.execute_2darray(sql) + if not status: + return internal_server_error(errormsg=result) + + for row in result['rows']: + return make_json_response( + data=self.blueprint.generate_browser_node( + row['oid'], + did, + row['name'], + icon="icon-subscription" + ), + status=200 + ) + + return gone(gettext("Could not find the specified subscription.")) + + @check_precondition + def properties(self, gid, sid, did, subid): + """ + This function will show the properties of the selected subscription + node. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + subid: Subscription ID + """ + status, res = self._fetch_properties(did, subid) + + if not status: + return res + + return ajax_response( + response=res, + status=200 + ) + + def _fetch_properties(self, did, subid): + """ + This function fetch the properties of the extension. + :param did: + :param subid: + :return: + """ + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + subid=subid, did=did, + ) + status, res = self.conn.execute_dict(sql) + + if not status: + return False, internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return False, gone(self._NOT_FOUND_PUB_INFORMATION) + + return True, res['rows'][0] + + @check_precondition + def update(self, gid, sid, did, subid): + """ + This function will update the data for the selected subscription node. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + subid: Subscription ID + """ + data = request.form if request.form else json.loads( + request.data, encoding='utf-8' + ) + + try: + if 'pub' in data: + data['pub'] = json.loads( + data['pub'], encoding='utf-8' + ) + sql, name = self.get_sql(data, subid) + # Most probably this is due to error + if not isinstance(sql, str): + return sql + sql = sql.strip('\n').strip(' ') + status, res = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res) + + return jsonify( + node=self.blueprint.generate_browser_node( + subid, + did, + name, + icon="icon-%s" % self.node_type + ) + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def create(self, gid, sid, did): + """ + This function will create the subscription object + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + """ + required_args = [ + 'name' + ] + + data = request.form if request.form else json.loads( + request.data, encoding='utf-8' + ) + for arg in required_args: + if arg not in data: + return make_json_response( + status=410, + success=0, + errormsg=gettext( + "Could not find the required parameter ({})." + ).format(arg) + ) + + try: + data['pub'] = json.loads( + data['pub'], encoding='utf-8' + ) + + sql = render_template("/".join([self.template_path, + self._CREATE_SQL]), + data=data, conn=self.conn) + status, res = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res) + + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + conn=self.conn, + did=did + ) + + status, r_set = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=r_set) + + for row in r_set['rows']: + return jsonify( + node=self.blueprint.generate_browser_node( + row['oid'], + did, + row['name'], + icon='icon-subscription' + ) + ) + + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def delete(self, gid, sid, did, subid=None): + """ + This function will drop the subscription object + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + subid: Subscription ID + only_sql: + """ + if subid is None: + data = request.form if request.form else json.loads( + request.data, encoding='utf-8' + ) + else: + data = {'ids': [subid]} + + cascade = self._check_cascade_operation() + + try: + for subid in data['ids']: + # Get name for subscription from subid + sql = render_template( + "/".join([self.template_path, self._DELETE_SQL]), + subid=subid, conn=self.conn + ) + status, subname = self.conn.execute_scalar(sql) + + if not status: + return internal_server_error(errormsg=subname) + + # drop subscription + sql = render_template( + "/".join([self.template_path, self._DELETE_SQL]), + subname=subname, cascade=cascade, conn=self.conn + ) + + status, res = self.conn.execute_scalar(sql) + if not status: + return internal_server_error(errormsg=res) + + return make_json_response( + success=1, + info=gettext("Subscription dropped") + ) + + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def msql(self, gid, sid, did, subid=None): + """ + This function is used to return modified SQL for the selected + subscription node. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + subid: Subscription ID + """ + data = {} + for k, v in request.args.items(): + try: + # comments should be taken as is because if user enters a + # json comment it is parsed by loads which should not happen + if k in ('description',): + data[k] = v + else: + data[k] = json.loads(v, encoding='utf-8') + except ValueError: + data[k] = v + try: + sql, name = self.get_sql(data, subid) + # Most probably this is due to error + if not isinstance(sql, str): + return sql + if sql == '': + sql = "--modified SQL" + + return make_json_response( + data=sql, + status=200 + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + def get_details(self, data, old_data): + """ + This function returns the required data to create subscription + :param data: + :return: + + """ + required_args = ['name'] + + required_connection_args = ['host', 'port', 'username', 'db', + 'service', 'passfile'] + for arg in required_args: + if arg not in data and arg in old_data: + data[arg] = old_data[arg] + + for arg in required_connection_args: + if arg not in data and arg in old_data: + data[arg] = old_data[arg] + + return data + + def get_sql(self, data, subid=None): + """ + This function will generate sql from model data. + + Args: + data: Contains the data of the selected subscription node. + subid: Subscription ID + """ + + required_args = ['name'] + + required_connection_args = ['host', 'port', 'username', 'db', + 'service', 'passfile'] + + if subid is not None: + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + subid=subid + ) + status, res = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return gone(self._NOT_FOUND_PUB_INFORMATION) + + old_data = res['rows'][0] + for arg in required_args: + if arg not in data: + data[arg] = old_data[arg] + + for arg in required_connection_args: + if arg in data: + old_data[arg] = data[arg] + + sql = render_template( + "/".join([self.template_path, self._UPDATE_SQL]), + data=data, o_data=old_data, conn=self.conn + ) + return sql.strip('\n'), data['name'] if 'name' in data \ + else old_data['name'] + else: + + sql = render_template("/".join([self.template_path, + self._CREATE_SQL]), + data=data, conn=self.conn) + return sql.strip('\n'), data['name'] + + def get_connection(self, connection_details): + + passfile = \ + connection_details['passfile'] if 'passfile' \ + in connection_details and \ + connection_details[ + 'passfile'] != '' else None + + service = \ + connection_details['service'] if 'service' in connection_details \ + and connection_details['service']\ + != '' else None + conn = psycopg2.connect( + host=connection_details['host'], + database=connection_details['db'], + user=connection_details['username'], + password=connection_details['password'], + port=connection_details['port'], + passfile=get_complete_file_path(passfile), + service=service + ) + # create a cursor + cur = conn.cursor() + cur.execute('SELECT pubname from pg_publication') + + publications = cur.fetchall() + # Close the connection + conn.close() + + return publications + + @check_precondition + def get_publications(self, gid, sid, did, *args, **kwargs): + """ + This function returns the publication list + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + """ + + url_params = None + if request.args: + url_params = {k: v for k, v in request.args.items()} + + if 'oid' in url_params: + status, params = self._fetch_properties(did, url_params['oid']) + params['password'] = url_params['password'] + url_params = params + + res = self.get_connection(url_params) + + result = [] + for pub in res: + result.append({ + "value": pub[0], + "label": pub[0] + }) + + return make_json_response( + data=result, + status=200 + ) + + @check_precondition + def sql(self, gid, sid, did, subid, json_resp=True): + """ + This function will generate sql to show in the sql pane for the + selected publication node. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + subid: Publication ID + json_resp: + """ + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + subid=subid + ) + status, res = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return gone(self._NOT_FOUND_PUB_INFORMATION) + + # Making copy of output for future use + old_data = dict(res['rows'][0]) + if old_data['slot_name'] is None and 'create_slot' not in old_data: + old_data['create_slot'] = False + + sql = render_template("/".join([self.template_path, + self._CREATE_SQL]), + data=old_data, conn=self.conn) + sql += "\n\n" + + sql_header = "-- Subscription : {};".format(old_data['name']) + sql_header += "\n" + + sql_header += "-- DROP SUBSCRIPTION {};".format(old_data['name']) + + sql_header += render_template( + "/".join([self.template_path, self._DELETE_SQL]), + sname=old_data['name'], ) + + sql_header += "\n" + + sql = sql_header + sql + + if not json_resp: + return sql + + return ajax_response(response=sql) + + @check_precondition + def dependents(self, gid, sid, did, subid): + """ + This function gets the dependents and returns an ajax response + for the subscription node. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + subid: Subscription ID + """ + dependents_result = self.get_dependents(self.conn, subid) + return ajax_response( + response=dependents_result, + status=200 + ) + + @check_precondition + def dependencies(self, gid, sid, did, subid): + """ + This function gets the dependencies and returns an ajax response + for the subscription node. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + subid: Subscription ID + """ + dependencies_result = self.get_dependencies(self.conn, subid) + return ajax_response( + response=dependencies_result, + status=200 + ) + + +SubscriptionView.register_node_view(blueprint) diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/img/coll-publication.svg b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/img/coll-publication.svg new file mode 100644 index 000000000..cc946919b --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/img/coll-publication.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/img/coll-subscription.svg b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/img/coll-subscription.svg new file mode 100644 index 000000000..d14adfd39 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/img/coll-subscription.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/img/publication.svg b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/img/publication.svg new file mode 100644 index 000000000..3b582cf51 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/img/publication.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/img/subscription.svg b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/img/subscription.svg new file mode 100644 index 000000000..63a153c6d --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/img/subscription.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.js b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.js new file mode 100644 index 000000000..4c47d2186 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.js @@ -0,0 +1,398 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2020, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +define('pgadmin.node.subscription', [ + 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', + 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', + 'sources/browser/server_groups/servers/model_validation', 'pgadmin.alertifyjs', 'pgadmin.browser.collection', +], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, modelValidation, Alertify) { + + // Extend the browser's collection class for subscriptions collection + if (!pgBrowser.Nodes['coll-subscription']) { + pgBrowser.Nodes['coll-subscription'] = + pgBrowser.Collection.extend({ + node: 'subscription', + label: gettext('Subscriptions'), + type: 'coll-subscription', + columns: ['name', 'subowner', 'pub', 'enabled'], + }); + } + + // Extend the browser's node class for subscription node + if (!pgBrowser.Nodes['subscription']) { + pgBrowser.Nodes['subscription'] = pgBrowser.Node.extend({ + parent_type: 'database', + type: 'subscription', + sqlAlterHelp: 'sql-altersubscription.html', + sqlCreateHelp: 'sql-createsubscription.html', + dialogHelp: url_for('help.static', {'filename': 'subscription_dialog.html'}), + label: gettext('Subscription'), + hasSQL: true, + canDrop: true, + canDropCascade: true, + hasDepends: true, + width: '510px', + Init: function() { + + // Avoid multiple registration of menus + if (this.initialized) + return; + + this.initialized = true; + + + // Add context menus for subscription + pgBrowser.add_menus([{ + name: 'create_subscription_on_database', node: 'database', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('Subscription...'), + icon: 'wcTabIcon icon-subscription', data: {action: 'create'}, + enable: pgBrowser.Nodes['database'].canCreate, + },{ + name: 'create_subscription_on_coll', node: 'coll-subscription', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('Subscription...'), + icon: 'wcTabIcon icon-subscription', data: {action: 'create'}, + enable: 'canCreate', + },{ + name: 'create_subscription', node: 'subscription', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('Subscription...'), + icon: 'wcTabIcon icon-subscription', data: {action: 'create'}, + enable: 'canCreate', + }]); + }, + // Define the model for subscription node + model: pgBrowser.Node.Model.extend({ + idAttribute: 'oid', + defaults: { + name: undefined, + subowner: undefined, + pubtable: undefined, + pub:[], + enabled:true, + create_slot: true, + copy_data:undefined, + connect:true, + sync:'off', + }, + + // Default values! + initialize: function(attrs, args) { + var isNew = (_.size(attrs) === 0); + if (isNew) { + var userInfo = pgBrowser.serverInfo[args.node_info.server._id].user; + + this.set({'subowner': userInfo.name}, {silent: true}); + } + pgBrowser.Node.Model.prototype.initialize.apply(this, arguments); + }, + + // Define the schema for the subscription node + schema: [{ + id: 'name', label: gettext('Name'), type: 'text', + mode: ['properties', 'create', 'edit'], + visible: function() { + if(!_.isUndefined(this.node_info) && !_.isUndefined(this.node_info.server) + && !_.isUndefined(this.node_info.server.version) && + this.node_info.server.version >= 100000) { + return true; + } + return false; + }, + },{ + id: 'oid', label: gettext('OID'), cell: 'string', mode: ['properties'], + type: 'text', + }, + { + id: 'subowner', label: gettext('Owner'), type: 'text', + control: Backform.NodeListByNameControl, node: 'role', + mode: ['edit', 'properties', 'create'], select2: { allowClear: false}, + }, + { + id: 'host', label: gettext('Host name/address'), type: 'text', group: gettext('Connection'), + mode: ['properties', 'edit', 'create'],disabled: 'isShared', + control: Backform.InputControl.extend({ + onChange: function() { + Backform.InputControl.prototype.onChange.apply(this, arguments); + if (!this.model || !this.model.changed) { + this.model.inform_text = undefined; + return; + } + }, + }), + },{ + id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'), + mode: ['properties', 'edit', 'create'], min: 1, max: 65535, disabled: 'isShared', + control: Backform.InputControl.extend({ + onChange: function() { + Backform.InputControl.prototype.onChange.apply(this, arguments); + if (!this.model || !this.model.changed) { + this.model.inform_text = undefined; + return; + } + }, + }), + },{ + id: 'username', label: gettext('Username'), type: 'text', group: gettext('Connection'), + mode: ['properties', 'edit', 'create'], + control: Backform.InputControl.extend({ + onChange: function() { + Backform.InputControl.prototype.onChange.apply(this, arguments); + if (!this.model || !this.model.changed) { + this.model.inform_text = undefined; + return; + } + }, + }), + },{ + id: 'password', label: gettext('Password'), type: 'password', maxlength: null, + group: gettext('Connection'), control: 'input', mode: ['create', 'edit'], deps: ['connect_now'], + },{ + id: 'db', label: gettext('Maintenance database'), type: 'text', group: gettext('Connection'), + mode: ['properties', 'edit', 'create'], + },{ + id: 'service', label: gettext('Service'), type: 'text', + mode: ['properties', 'edit', 'create'], + group: gettext('Connection'), + }, + { + id: 'passfile', label: gettext('Passfile'), type: 'text', group: gettext('Connection'), + mode: ['properties', 'edit', 'create'], + }, + { + id: 'pub', label: gettext('Publication'), type: 'text', group: gettext('Connection'), + mode: ['properties'], + }, + { + id: 'cur_pub', label: gettext('Current publication'), type: 'text', group: gettext('Connection'), + mode: ['edit'], disabled:true, + }, + { + id: 'pub', label: gettext('Publication'), type: 'array', select2: { allowClear: true, multiple: true, width: '91%'}, + group: gettext('Connection'), mode: ['create', 'edit'],controlsClassName: 'pgadmin-controls pg-el-sm-11 pg-el-12', + deps: ['all_table'], + helpMessage: gettext('Clicks on the button to get Publication'), + control: Backform.Select2Control.extend({ + defaults: _.extend(Backform.Select2Control.prototype.defaults, { + select2: { + allowClear: true, + selectOnBlur: true, + tags: true, + placeholder: gettext('Select an item...'), + width: 'style', + }, + }), + template: _.template([ + '', + '
', + '
', + ' ', + '
', + '', + '
', + '
', + '<% if (helpMessage && helpMessage.length) { %>', + '<%=helpMessage%>', + '<% } %>', + '
', + ].join('\n')), + + events: _.extend({}, Backform.Select2Control.prototype.events(), { + 'click .get_publication': 'getPublication', + }), + + render: function(){ + return Backform.Select2Control.prototype.render.apply(this, arguments); + }, + + getPublication: function() { + var self = this; + var publication_url = pgBrowser.Nodes['database'].generate_url.apply( + pgBrowser.Nodes['subscription'], [ + null, 'get_publications', this.field.get('node_data'), null, + this.field.get('node_info'), pgBrowser.Nodes['database'].url_jump_after_node, + ]); + var result = ''; + + $.ajax({ + url: publication_url, + type: 'GET', + data: self.model.toJSON(true, 'GET'), + dataType: 'json', + contentType: 'application/json', + }) + .done(function(res) { + result = res.data; + self.field.set('options', result); + Backform.Select2Control.prototype.render.apply(self, arguments); + + var transform = self.field.get('transform') || self.defaults.transform; + if (transform && _.isFunction(transform)) { + self.field.set('options', transform.bind(self, result)); + } else { + self.field.set('options', result); + } + + }) + .fail(function(res) { + Alertify.alert( + gettext('Check connection?'), + gettext(res.responseJSON.errormsg) + ); + }); + }, + }), + }, + { + id: 'copy_data', label: gettext('Copy data'), + type: 'switch', mode: ['create','edit'], + group: gettext('With'), + 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', + readonly: 'isConnect', deps :['connect'], + }, + { + id: 'create_slot', label: gettext('Create slot'), + type: 'switch', mode: ['create'], + group: gettext('With'), + 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: 'isDisable', + readonly: 'isConnect', deps :['connect'], + + }, + { + id: 'enabled', label: gettext('Enabled'), + type: 'switch', mode: ['create','edit', 'properties'], + group: gettext('With'), + 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', + readonly: 'isConnect', deps :['connect'], + }, + { + id: 'refresh_pub', label: gettext('Refresh publication'), + type: 'switch', mode: ['edit'], + group: gettext('With'), + 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', + },{ + id: 'connect', label: gettext('Connect'), + type: 'switch', mode: ['create'], + group: gettext('With'), + 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: 'isDisable', deps:['enabled', 'create_slot', 'copy_data'], + }, + { + id: 'slot_name', label: gettext('Slot name'), + type: 'text', mode: ['create','edit', 'properties'], + group: gettext('With'), + }, + { + id: 'sync', label: gettext('Synchronous commit'), control: 'select2', deps:['event'], + group: gettext('With'), type: 'text', + select2: { + width: '100%', + allowClear: false, + }, + options:[ + {label: 'local', value: 'local'}, + {label: 'remote_write', value: 'remote_write'}, + {label: 'remote_apply', value: 'remote_apply'}, + {label: 'on', value: 'on'}, + {label: 'off', value: 'off'}, + ], + }, + ], + isShared: function(){ + return false; + }, + isDisable:function(m){ + if (m.isNew()) + return false; + return true; + }, + + isConnect: function(m){ + if(!m.get('connect')){ + setTimeout( function() { + m.set('copy_data', false); + m.set('create_slot', false); + m.set('enabled', false); + }, 10); + return true; + } + return false; + }, + + + /* validate function is used to validate the input given by + * the user. In case of error, message will be displayed on + * the GUI for the respective control. + */ + validate: function() { + var msg; + this.errorModel.clear(); + var name = this.get('name'); + var pub = this.get('pub'); + + if (_.isUndefined(name) || _.isNull(name) || + String(name).replace(/^\s+|\s+$/g, '') == '') { + msg = gettext('Name cannot be empty.'); + this.errorModel.set('name', msg); + return msg; + } + if (_.isUndefined(pub) || _.isNull(pub) || + String(pub).replace(/^\s+|\s+$/g, '') == '') { + msg = gettext('Publication cannot be empty.'); + this.errorModel.set('pub', msg); + return msg; + } + + const validateModel = new modelValidation.ModelValidation(this); + return validateModel.validate(); + }, + canCreate: function(itemData, item) { + var treeData = this.getTreeNodeHierarchy(item), + server = treeData['server']; + + // If server is less than 10 then do not allow 'create' menu + if (server && server.version < 100000) + return false; + + // by default we want to allow create menu + return true; + }, + + }), + }); + } + return pgBrowser.Nodes['coll-subscription']; +}); diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/acl.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/acl.sql new file mode 100644 index 000000000..9054baa45 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/acl.sql @@ -0,0 +1,24 @@ +SELECT 'lanacl' as deftype, COALESCE(gt.rolname, 'PUBLIC') grantee, g.rolname grantor, + array_agg(privilege_type) as privileges, array_agg(is_grantable) as grantable +FROM + (SELECT + d.grantee, d.grantor, d.is_grantable, + CASE d.privilege_type + WHEN 'USAGE' THEN 'U' + ELSE 'UNKNOWN' + END AS privilege_type + FROM + (SELECT lanacl FROM pg_language lan + LEFT OUTER JOIN pg_shdescription descr ON (lan.oid=descr.objoid AND descr.classoid='pg_language'::regclass) + WHERE lan.oid = {{ lid|qtLiteral }}::OID + ) acl, + (SELECT (d).grantee AS grantee, (d).grantor AS grantor, (d).is_grantable AS is_grantable, + (d).privilege_type AS privilege_type + FROM (SELECT aclexplode(lanacl) as d FROM pg_language lan1 + WHERE lan1.oid = {{ lid|qtLiteral }}::OID ) a + ) d + ) d +LEFT JOIN pg_catalog.pg_roles g ON (d.grantor = g.oid) +LEFT JOIN pg_catalog.pg_roles gt ON (d.grantee = gt.oid) +GROUP BY g.rolname, gt.rolname +ORDER BY grantee diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/create.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/create.sql new file mode 100644 index 000000000..863f6631c --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/create.sql @@ -0,0 +1,24 @@ +{% if data.copy_data is defined or data.create_slot is defined or data.slot_name is defined or data.sync is defined %} +{% set add_semicolon_after_enabled = 'enabled' %} +{% endif %} +{% if data.create_slot is defined or data.slot_name is defined %} +{% set add_semicolon_after_copy_data = 'copy_data' %} +{% endif %} +{% if data.slot_name is defined or data.sync is defined %} +{% set add_semicolon_after_create_slot = 'create_slot' %} +{% endif %} +{% if data.sync is defined %} +{% set add_semicolon_after_slot_name = 'slot_name' %} +{% endif %} + +CREATE SUBSCRIPTION {{ conn|qtIdent(data.name) }} +{% if data.host or data.port or data.username or data.password or data.db %} +CONNECTION '{% if data.host %}host={{ data.host}}{% endif %}{% if data.port %} port={{ data.port }}{% endif %}{% if data.username %} user={{ data.username }}{% endif %}{% if data.db %} dbname={{ data.db }}{% endif %}{% if data.service %} service={{ data.service }}{% endif %}{% if data.passfile %} passfile={{ data.passfile }}{% endif %}{% if data.password %} password={{ data.password}}{% endif %}' +{% endif %} +{% if data.pub %} +PUBLICATION {% for pub in data.pub %}{% if loop.index != 1 %},{% endif %}{{ conn|qtIdent(pub) }}{% endfor %} +{% endif %} + +with ({% if data.connect is defined %}connect = {{ data.connect}}, {% endif %}enabled = {{ data.enabled}},{% if data.copy_data %}copy_data = {{ data.copy_data}}{% if add_semicolon_after_copy_data == 'copy_data' %},{% endif %}{% endif %} +{% if data.create_slot is defined %} create_slot = {{ data.create_slot }}{% if add_semicolon_after_create_slot == 'create_slot' %},{% endif %}{% endif %} +{% if data.slot_name is defined and data.slot_name != ''%} slot_name = {{ data.slot_name }}{% if add_semicolon_after_slot_name == 'slot_name' %},{% endif %}{% endif %}{% if data.sync %} synchronous_commit = '{{ data.sync }}'{% endif %}); diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/delete.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/delete.sql new file mode 100644 index 000000000..33942fa99 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/delete.sql @@ -0,0 +1,8 @@ +{# ============= Get the subscription name using oid ============= #} +{% if subid %} + SELECT subname FROM pg_subscription WHERE oid = {{subid}}::oid; +{% endif %} +{# ============= Drop the language ============= #} +{% if subname %} +DROP SUBSCRIPTION {{ conn|qtIdent(subname) }}{% if cascade %} CASCADE{% endif%}; +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/functions.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/functions.sql new file mode 100644 index 000000000..b493abc7b --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/functions.sql @@ -0,0 +1,6 @@ +SELECT quote_ident(c.table_schema)||'.'||quote_ident(c.table_name) AS table +from information_schema.tables c +where c.table_type = 'BASE TABLE' + AND c.table_schema NOT LIKE 'pg\_%' + AND c.table_schema NOT LIKE 'pgagent' + AND c.table_schema NOT IN ('information_schema') ORDER BY 1; diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/nodes.sql new file mode 100644 index 000000000..10f841743 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/nodes.sql @@ -0,0 +1,7 @@ +select oid, sub.subname as name from pg_subscription sub +WHERE +{% if subid %} + sub.oid = {{ subid }}; +{% else %} + sub.subdbid = {{ did }}; +{% endif %}; diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/properties.sql new file mode 100644 index 000000000..7dd18b517 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/properties.sql @@ -0,0 +1,21 @@ +SELECT sub.oid as oid, + subname as name, + subpublications as pub, + sub.subsynccommit as sync, + subpublications as cur_pub, + pga.rolname as subowner, + subslotname as slot_name, + subenabled as enabled, + SPLIT_PART(SPLIT_PART(subconninfo,'port',1), '=',2) as host, + SPLIT_PART(SPLIT_PART(subconninfo,'port=',2), ' ',1) as port, + SPLIT_PART(SPLIT_PART(subconninfo,'user=',2), ' ',1) as username, + SPLIT_PART(SPLIT_PART(subconninfo,'dbname=',2), ' ',1) as db, + SPLIT_PART(SPLIT_PART(subconninfo,'service=',2), ' ',1) as service, + SPLIT_PART(SPLIT_PART(subconninfo,'passfile=',2), ' ',1) as passfile +FROM pg_subscription sub join pg_authid pga on sub.subowner= pga.oid +WHERE +{% if subid %} + sub.oid = {{ subid }}; +{% else %} + sub.subdbid = {{ did }}; +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/sqlpane.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/sqlpane.sql new file mode 100644 index 000000000..e5ac14a75 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/sqlpane.sql @@ -0,0 +1,42 @@ +{% import 'macros/privilege.macros' as PRIVILEGE %} +{% import 'macros/security.macros' as SECLABEL %} +-- Language: {{data.name}} + +-- DROP LANGUAGE {{ conn|qtIdent(data.name) }} + +{# ============= CREATE LANGUAGE Query ============= #} +CREATE{% if data.trusted %} TRUSTED{% endif %} PROCEDURAL LANGUAGE {{ conn|qtIdent(data.name) }} +{% if data.lanproc %} + HANDLER {{ conn|qtIdent(data.lanproc) }} +{% endif %} +{% if data.laninl %} + INLINE {{ conn|qtIdent(data.laninl) }} +{% endif %} +{% if data.lanval %} + VALIDATOR {{ conn|qtIdent(data.lanval) }}{% endif %}; +{# ============= ALTER LANGUAGE Query ============= #} +{% if data.lanowner %} + +ALTER LANGUAGE {{ conn|qtIdent(data.name) }} + OWNER TO {{ conn|qtIdent(data.lanowner) }}; +{% endif %} +{# ============= Comment on LANGUAGE Query ============= #} +{% if data.description %} + +COMMENT ON LANGUAGE {{ conn|qtIdent(data.name) }} + IS {{ data.description|qtLiteral }}; +{% endif %} +{# ============= PRIVILEGES on LANGUAGE ============= #} +{% if data.lanacl and data.lanacl|length > 0 %} + +{% for priv in data.lanacl %} +{{ PRIVILEGE.APPLY(conn, 'LANGUAGE', priv.grantee, data.name, priv.without_grant, priv.with_grant) }} +{% endfor %} +{% endif %} +{# ============= PRIVILEGES on LANGUAGE ============= #} +{% if data.seclabels and data.seclabels|length > 0 %} + +{% for r in data.seclabels %} +{{ SECLABEL.APPLY(conn, 'PROCEDURAL LANGUAGE', data.name, r.provider, r.label) }} +{% endfor %} +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/templates.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/templates.sql new file mode 100644 index 000000000..f67184af5 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/templates.sql @@ -0,0 +1,7 @@ +{# ============= SELECT Language templates ============= #} +SELECT + tmplname +FROM pg_pltemplate +LEFT JOIN pg_language ON tmplname=lanname +WHERE lanname IS NULL +ORDER BY tmplname; \ No newline at end of file diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/update.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/update.sql new file mode 100644 index 000000000..ad8ad91f7 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/update.sql @@ -0,0 +1,52 @@ +{% if data.sync is defined %} +{% set add_semicolon_after_slot_name = 'slot_name' %} +{% endif %} + +{#####################################################} +{## Change owner of subscription ##} +{#####################################################} +{% if data.subowner and data.subowner != o_data.subowner %} +ALTER SUBSCRIPTION {{ conn|qtIdent(o_data.name) }} OWNER TO {{ data.subowner }}; +{% endif %} + +{% if data.name and data.name != o_data.name %} +ALTER SUBSCRIPTION {{ conn|qtIdent(o_data.name) }} RENAME TO {{ conn|qtIdent(data.name) }}; +{% endif %} + +{% if data.enabled is defined and data.enabled != o_data.enabled %} +{% if not data.enabled %} +ALTER SUBSCRIPTION {{ conn|qtIdent(o_data.name) }} DISABLE; +{% endif %} +{% endif %} + +{% if data.slot_name or data.sync %} +ALTER SUBSCRIPTION {{ conn|qtIdent(o_data.name) }} SET ({% if data.slot_name %}slot_name = {{ data.slot_name }}{% if add_semicolon_after_slot_name == 'slot_name' %},{% endif %}{% endif %}{% if data.sync %} synchronous_commit = '{{ data.sync }}'{% endif %}); +{% endif %} + +{% if data.enabled is defined and data.enabled != o_data.enabled %} +{% if data.enabled %} +ALTER SUBSCRIPTION {{ conn|qtIdent(o_data.name) }} ENABLE; +{% endif %} +{% endif %} + +{% if data.refresh_pub %} +ALTER SUBSCRIPTION {{ conn|qtIdent(o_data.name) }} REFRESH PUBLICATION{% if not data.copy_data %} WITH (copy_data = False){% else %} WITH (copy_data = True){% endif %}; +{% endif %} + +{% if data.pub%} +{% if data.pub and not data.refresh_pub and not data.enabled %} +ALTER SUBSCRIPTION {{ conn|qtIdent(o_data.name) }} SET PUBLICATION {% for pub in data.pub %}{% if loop.index != 1 %},{% endif %}{{ conn|qtIdent(pub) }}{% endfor %} WITH (refresh = false); +{% else %} +ALTER SUBSCRIPTION {{ conn|qtIdent(o_data.name) }} SET PUBLICATION {% for pub in data.pub %}{% if loop.index != 1 %},{% endif %}{{ conn|qtIdent(pub) }}{% endfor %}; +{% endif %} +{% endif %} + +{% if data.host or data.port or data.username or data.password or data.db or data.service or data.passfile %} +ALTER SUBSCRIPTION {{ conn|qtIdent(o_data.name) }} CONNECTION 'host={{ o_data.host}} port={{ o_data.port }} user={{ o_data.username }} dbname={{ o_data.db }}{% if data.service %} service={{ o_data.service }}{% endif %}{% if data.passfile %} passfile={{ o_data.passfile }}{% endif %}{% if data.password %} password={{ data.password}}{% endif %}'; +{% endif %} + + + + + + diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/alter_publication_event.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/alter_publication_event.sql new file mode 100644 index 000000000..f6298f2ae --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/alter_publication_event.sql @@ -0,0 +1,8 @@ +-- Publication : alterd_publication; +-- DROP PUBLICATION alterd_publication; + + +CREATE PUBLICATION alterd_publication + FOR ALL TABLES + +with (publish = 'insert,update'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/alter_subscription.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/alter_subscription.sql new file mode 100644 index 000000000..4058445d3 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/alter_subscription.sql @@ -0,0 +1,7 @@ +-- Subscription : test_alter_subscription; +-- DROP SUBSCRIPTION test_alter_subscription; + +CREATE SUBSCRIPTION test_alter_subscription +CONNECTION 'host=localhost port=5432 user=postgres dbname=postgres' +PUBLICATION sample__1 +with (enabled = False, create_slot = False, slot_name = None, synchronous_commit = 'off'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/create_publication_update.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/create_publication_update.sql new file mode 100644 index 000000000..e4ae78ff8 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/create_publication_update.sql @@ -0,0 +1,8 @@ +-- Publication : test_publication_with_update; +-- DROP PUBLICATION test_publication_with_update; + + +CREATE PUBLICATION test_publication_with_update + FOR ALL TABLES + +with (publish = 'update'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/create_publication_update_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/create_publication_update_msql.sql new file mode 100644 index 000000000..33abf9457 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/create_publication_update_msql.sql @@ -0,0 +1,4 @@ +CREATE PUBLICATION test_publication_with_update + FOR ALL TABLES + +with (publish = 'update'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/create_subscription.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/create_subscription.sql new file mode 100644 index 000000000..23f41ceae --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/create_subscription.sql @@ -0,0 +1,7 @@ +-- Subscription : test_create_subscription; +-- DROP SUBSCRIPTION test_create_subscription; + +CREATE SUBSCRIPTION test_create_subscription +CONNECTION 'host=localhost port=5432 user=postgres dbname=postgres' +PUBLICATION sample__1 +with (enabled = False, create_slot = False, slot_name = None, synchronous_commit = 'off'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/create_subscription_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/create_subscription_msql.sql new file mode 100644 index 000000000..e1cd83a05 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/create_subscription_msql.sql @@ -0,0 +1,4 @@ +CREATE SUBSCRIPTION test_create_subscription +CONNECTION 'host=localhost port=5432 user=postgres dbname=postgres password=edb' +PUBLICATION sample__1 +with (connect = False, enabled = False, create_slot = False, slot_name = None, synchronous_commit = 'off'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/test.json b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/test.json new file mode 100644 index 000000000..7f1406738 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/10_plus/test.json @@ -0,0 +1,49 @@ +{ + "scenarios": [ + { + "type": "create", + "name": "Create Subscription", + "endpoint": "NODE-subscription.obj", + "sql_endpoint": "NODE-subscription.sql_id", + "msql_endpoint": "NODE-subscription.msql", + "data": { + "username": "postgres", + "name": "test_create_subscription", + "connect": false, + "copy_data": false, + "create_slot": false, + "db": "postgres", + "subowner": "postgres", + "enabled": false, + "host": "localhost", + "slot_name": "None", + "service": "", + "port": 5432, + "password": "edb", + "sync": "off", + "pub": "[\"sample__1\"]" + }, + "expected_sql_file": "create_subscription.sql", + "expected_msql_file": "create_subscription_msql.sql" + }, + { + "type": "alter", + "name": "Alter Subscription", + "endpoint": "NODE-subscription.obj_id", + "sql_endpoint": "NODE-subscription.sql_id", + "data": { + "name": "test_alter_subscription" + }, + "expected_sql_file": "alter_subscription.sql" + }, + + { + "type": "delete", + "name": "Drop subscription", + "endpoint": "NODE-subscription.delete_id", + "data": { + "name": "test_alter_subscription" + } + } + ] +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/__init__.py new file mode 100644 index 000000000..27cd89067 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/__init__.py @@ -0,0 +1,16 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from pgadmin.utils.route import BaseTestGenerator + + +class RulesTestGenerator(BaseTestGenerator): + + def runTest(self): + return [] diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/subscription_test_data.json b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/subscription_test_data.json new file mode 100644 index 000000000..6f8acd270 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/subscription_test_data.json @@ -0,0 +1,432 @@ +{ + "add_subscription": [ + { + "name": "Create subscription with insert and update", + "url": "/browser/subscription/obj/", + "is_positive_test": true, + "test_data": { + "username": "postgres", + "name": "PLACEHOLDER", + "connect": false, + "copy_data": false, + "create_slot": false, + "db": "postgres", + "subowner": "postgres", + "enabled": false, + "host": "localhost", + "slot_name": "NONE", + "service": "", + "port": 5432, + "password": "", + "sync": "off", + "pub": "PLACE_HOLDER" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Create subscription for few tables", + "url": "/browser/subscription/obj/", + "is_positive_test": true, + "get_publication": true, + "test_data": { + "username": "postgres", + "name": "PLACEHOLDER", + "connect": false, + "copy_data": false, + "create_slot": false, + "db": "postgres", + "subowner": "postgres", + "enabled": false, + "host": "localhost", + "slot_name": "NONE", + "service": "", + "port": 5432, + "password": "", + "sync": "off", + "pub": "PLACE_HOLDER" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Create a subscription without name", + "url": "/browser/subscription/obj/", + "is_positive_test": false, + "without_name": true, + "test_data": { + "username": "postgres", + "name": "PLACEHOLDER", + "connect": false, + "copy_data": false, + "create_slot": false, + "db": "postgres", + "subowner": "postgres", + "enabled": false, + "host": "localhost", + "slot_name": "NONE", + "service": "", + "port": 5432, + "password": "", + "sync": "off", + "pub": "PLACE_HOLDER" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 410 + } + }, + { + "name": "Error while adding a subscription", + "url": "/browser/subscription/obj/", + "is_positive_test": false, + "error_creating_subscription": true, + "test_data": { + "username": "postgres", + "name": "PLACEHOLDER", + "connect": false, + "copy_data": false, + "create_slot": false, + "db": "postgres", + "subowner": "postgres", + "enabled": false, + "host": "localhost", + "slot_name": "NONE", + "service": "", + "port": 5432, + "password": "", + "sync": "off", + "pub": "PLACE_HOLDER" + }, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(False, 'Mocked Internal Server Error ')" + }, + "expected_data": { + "status_code": 500 + } + }, + { + "name": "Exception while adding a subscription", + "url": "/browser/subscription/obj/", + "is_positive_test": false, + "test_data": { + "username": "postgres", + "name": "PLACEHOLDER", + "connect": false, + "copy_data": false, + "create_slot": false, + "db": "postgres", + "subowner": "postgres", + "enabled": false, + "host": "localhost", + "slot_name": "NONE", + "service": "", + "port": 5432, + "password": "", + "sync": "off", + "pub": "PLACE_HOLDER" + }, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(True, True)(False, 'Mocked Internal Server Error ')" + }, + "expected_data": { + "status_code": 500 + } + } + ], + "get_subscription": [ + { + "name": "Get a subscription URL", + "url": "/browser/subscription/obj/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while fetching a subscription properties", + "url": "/browser/subscription/obj/", + "is_positive_test": false, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + }, + { + "name": "Get a subscription properties under database nodes", + "url": "/browser/subscription/obj/", + "is_positive_test": true, + "database_nodes": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while fetching a subscription properties under database nodes", + "url": "/browser/subscription/obj/", + "is_positive_test": false, + "database_nodes": true, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + }, + { + "name": "Get a subscription Node", + "url": "/browser/subscription/nodes/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get a subscription Node dependants", + "url": "/browser/subscription/dependent/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get a subscription Node dependency", + "url": "/browser/subscription/dependency/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while fetching the subscription under the database nodes using wrong database id", + "url": "/browser/subscription/nodes/", + "is_positive_test": false, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_2darray", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + }, + { + "name": "Get all the subscription under the database nodes", + "url": "/browser/subscription/nodes/", + "is_positive_test": true, + "mocking_required": false, + "database_nodes": true, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get all the subscription under the database nodes using wrong database id", + "url": "/browser/subscription/nodes/", + "is_positive_test": true, + "incorrect_database_id": true, + "database_nodes": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while fetching all the subscription under the database nodes using wrong database id", + "url": "/browser/subscription/nodes/", + "is_positive_test": false, + "database_nodes": true, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_2darray", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + } + ], + "delete_subscription": [ + { + "name": "Delete a subscription URL", + "url": "/browser/subscription/obj/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while deleting the subscription", + "url": "/browser/subscription/obj/", + "is_positive_test": false, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_scalar", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + } + ], + "update_subscription": [ + { + "name": "update a subscription name", + "url": "/browser/subscription/obj/", + "is_positive_test": true, + "update_name": true, + "test_data": { + "name": "PLACE_HOLDER", + "id": "PLACE_HOLDER" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "update a subscription event insert", + "url": "/browser/subscription/obj/", + "is_positive_test": true, + "owner_subscription": true, + "test_data": { + "id": "PLACE_HOLDER", + "port": "5444" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "update a subscription event delete", + "url": "/browser/subscription/obj/", + "is_positive_test": true, + "owner_subscription": true, + "test_data": { + "id": "PLACE_HOLDER", + "port": "4444" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while fetching a subscription to update", + "url": "/browser/subscription/obj/", + "is_positive_test": false, + "mocking_required": true, + "test_data": { + "name": "PLACE_HOLDER", + "id": "PLACE_HOLDER" + }, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + }, + { + "name": "Error while fetching a subscription to update using wrong subscription id", + "url": "/browser/subscription/obj/", + "is_positive_test": true, + "wrong_subscription_id": true, + "mocking_required": false, + "test_data": { + "id": "PLACE_HOLDER" + }, + "mock_data": {}, + "expected_data": { + "status_code": 500 + } + } + ], + "delete_multiple_subscription": [ + { + "name": "Delete multiple subscription", + "url": "/browser/subscription/obj/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + } + ], + "sql_subscription": [ + { + "name": "Fetch the subscription SQL", + "url": "/browser/subscription/sql/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": { + }, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while fetching a subscription SQL", + "url": "/browser/subscription/sql/", + "is_positive_test": false, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + }, + { + "name": "Fetching a subscription mSQL", + "url": "/browser/subscription/msql/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(False, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 200 + } + } + ] +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_create.py b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_create.py new file mode 100644 index 000000000..73bd353b8 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_create.py @@ -0,0 +1,97 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import json +import uuid +from unittest.mock import patch + +from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \ + import utils as tables_utils +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 . import utils as subscription_utils + + +class PublicationsAddTestCase(BaseTestGenerator): + """This class will add new subscription""" + scenarios = utils.generate_scenarios('add_subscription', + subscription_utils.test_cases) + + def setUp(self): + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + self.schema_name = schema_info["schema_name"] + self.server_version = schema_info["server_version"] + if self.server_version < 99999: + self.skipTest( + "Logical replication is not supported " + "for server version less than 10" + + ) + + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception( + "Could not connect to database to add a subscription.") + + if self.is_positive_test and hasattr(self, 'few_tables_11'): + self.table_name = "table_column_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = tables_utils.create_table(self.server, + self.db_name, + self.schema_name, + self.table_name) + + self.test_data['pubtable'] = subscription_utils.get_tables(self) + + def runTest(self): + """This function will subscription.""" + self.test_data['name'] = \ + "test_subscription_add_%s" % (str(uuid.uuid4())[1:8]) + + self.test_data['pub'] = """["sample__1"]""" + + data = self.test_data + if self.is_positive_test: + response = self.create_subscription() + else: + if hasattr(self, 'without_name'): + del data["name"] + response = self.create_subscription() + elif hasattr(self, 'error_creating_subscription'): + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + response = self.create_subscription() + else: + with patch(self.mock_data["function_name"], + side_effect=self.mock_data["return_value"]): + response = self.create_subscription() + self.assertEqual(response.status_code, + self.expected_data["status_code"]) + + def create_subscription(self): + return self.tester.post( + self.url + str(utils.SERVER_GROUP) + '/' + + str(self.server_id) + '/' + str( + self.db_id) + '/', + data=json.dumps(self.test_data), + content_type='html/json') + + def tearDown(self): + if self.is_positive_test: + subscription_utils.delete_subscription(self.server, self.db_name, + self.test_data['name']) + + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_delete.py b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_delete.py new file mode 100644 index 000000000..7cd2aafce --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_delete.py @@ -0,0 +1,93 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +from unittest.mock import patch + +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +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 . import utils as subscription_utils + + +class PublicationDeleteTestCase(BaseTestGenerator): + """This class will delete subscription.""" + scenarios = utils.generate_scenarios('delete_subscription', + subscription_utils.test_cases) + + def setUp(self): + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + self.server_version = schema_info["server_version"] + if self.server_version < 99999: + self.skipTest( + "Logical replication is not supported " + "for server version less than 10" + + ) + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception( + "Could not connect to database to delete subscription.") + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to delete " + "subscription.") + self.subscription_name = "test_subscription_delete_%s" % ( + str(uuid.uuid4())[1:8]) + + self.subscription_id = subscription_utils.create_subscription( + self.server, + self.db_name, + self.subscription_name) + + def delete_subscription(self): + return self.tester.delete( + self.url + str(utils.SERVER_GROUP) + '/' + + str(self.server_id) + '/' + str(self.db_id) + + '/' + str(self.subscription_id), + follow_redirects=True) + + def runTest(self): + """This function will delete subscription""" + subscription_response = subscription_utils.verify_subscription( + self.server, + self.db_name, + self.subscription_name) + if not subscription_response: + raise Exception("Could not find the subscription to delete.") + + if self.is_positive_test: + if hasattr(self, "invalid_subscription_id"): + self.subscription_id = 9999 + response = self.delete_subscription() + else: + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + response = self.delete_subscription() + + self.assertEqual(response.status_code, + self.expected_data["status_code"]) + + def tearDown(self): + # Disconnect the database + subscription_utils.delete_subscription(self.server, self.db_name, + self.subscription_name) + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_delete_multiple.py b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_delete_multiple.py new file mode 100644 index 000000000..ea93c06f2 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_delete_multiple.py @@ -0,0 +1,106 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +import json + +from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \ + import utils as tables_utils +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +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 . import utils as subscription_utils + + +class PublicationDeleteTestCases(BaseTestGenerator): + """This class will delete subscription.""" + + scenarios = utils.generate_scenarios('delete_multiple_subscription', + subscription_utils.test_cases) + + def setUp(self): + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + self.server_version = schema_info["server_version"] + if self.server_version < 99999: + self.skipTest( + "Logical replication is not supported " + "for server version less than 10" + + ) + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception( + "Could not connect to database to delete subscription.") + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to " + "delete subscription.") + self.table_name = "table_column_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = tables_utils.create_table(self.server, self.db_name, + self.schema_name, + self.table_name) + self.subscription_name = "test_subscription_delete_%s" % ( + str(uuid.uuid4())[1:8]) + self.subscription_name_1 = "test_subscription_delete_%s" % ( + str(uuid.uuid4())[1:8]) + self.subscription_ids = [ + subscription_utils.create_subscription(self.server, self.db_name, + self.subscription_name), + subscription_utils.create_subscription(self.server, self.db_name, + self.subscription_name_1), + ] + + def delete_multiple_subscription(self, data): + return self.tester.delete( + "{0}{1}/{2}/{3}/".format(self.url, utils.SERVER_GROUP, + self.server_id, self.db_id + ), + follow_redirects=True, + data=json.dumps(data), + content_type='html/json' + ) + + def runTest(self): + """This function will delete subscription.""" + subscription_response = subscription_utils.verify_subscription( + self.server, + self.db_name, + self.subscription_name) + if not subscription_response: + raise Exception("Could not find the subscription to delete.") + + subscription_response = subscription_utils.verify_subscription( + self.server, + self.db_name, + self.subscription_name_1) + if not subscription_response: + raise Exception("Could not find the subscription to delete.") + + data = {'ids': self.subscription_ids} + if self.is_positive_test: + response = self.delete_multiple_subscription(data) + self.assertEqual(response.status_code, + self.expected_data["status_code"]) + + def tearDown(self): + # Disconnect the database + + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_get.py b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_get.py new file mode 100644 index 000000000..b607c3399 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_get.py @@ -0,0 +1,99 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +from unittest.mock import patch + +from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \ + import utils as tables_utils +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +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 . import utils as subscription_utils + + +class PublicationGetTestCase(BaseTestGenerator): + """This class will fetch the subscription under table node.""" + scenarios = utils.generate_scenarios('get_subscription', + subscription_utils.test_cases) + + def setUp(self): + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + self.server_version = schema_info["server_version"] + if self.server_version < 99999: + self.skipTest( + "Logical replication is not supported " + "for server version less than 10" + + ) + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception("Could not connect to database to " + "delete subscription.") + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to delete " + "subscription.") + self.table_name = "table_column_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = tables_utils.create_table(self.server, self.db_name, + self.schema_name, + self.table_name) + self.subscription_name = "test_subscription_delete_%s" % ( + str(uuid.uuid4())[1:8]) + self.subscription_id = subscription_utils.create_subscription( + self.server, self.db_name, + self.subscription_name) + + def get_subscription(self): + return self.tester.get( + self.url + str(utils.SERVER_GROUP) + '/' + str( + self.server_id) + '/' + + str(self.db_id) + '/' + str(self.subscription_id), + content_type='html/json') + + def runTest(self): + """This function will fetch the subscription.""" + + if self.is_positive_test: + if hasattr(self, "database_nodes"): + self.subscription_id = '' + response = self.get_subscription() + else: + response = self.get_subscription() + else: + if hasattr(self, "database_nodes"): + self.subscription_id = '' + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + response = self.get_subscription() + else: + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + response = self.get_subscription() + + self.assertEqual(response.status_code, + self.expected_data["status_code"]) + + def tearDown(self): + # Disconnect the database + subscription_utils.delete_subscription(self.server, self.db_name, + self.subscription_name) + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_put.py b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_put.py new file mode 100644 index 000000000..2d479b1a3 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_put.py @@ -0,0 +1,112 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import json +import uuid +from unittest.mock import patch + +from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \ + import utils as tables_utils +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +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 . import utils as subscription_utils + + +class PublicationUpdateTestCase(BaseTestGenerator): + """This class will update the subscription.""" + scenarios = utils.generate_scenarios('update_subscription', + subscription_utils.test_cases) + + def setUp(self): + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + self.server_version = schema_info["server_version"] + if self.server_version < 99999: + self.skipTest( + "Logical replication is not supported " + "for server version less than 10" + + ) + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception( + "Could not connect to database to delete subscription.") + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to delete " + "subscription.") + self.table_name = "table_column_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = tables_utils.create_table(self.server, self.db_name, + self.schema_name, + self.table_name) + self.subscription_name = "test_subscription_update_%s" % ( + str(uuid.uuid4())[1:8]) + self.subscription_id = \ + subscription_utils.create_subscription(self.server, self.db_name, + self.subscription_name) + + def update_subscription(self, data): + return self.tester.put( + self.url + str(utils.SERVER_GROUP) + '/' + + str(self.server_id) + '/' + str( + self.db_id) + + '/' + str(self.subscription_id), + data=json.dumps(data), + follow_redirects=True) + + def runTest(self): + """This function will update the subscription.""" + subscription_name = \ + subscription_utils.verify_subscription(self.server, self.db_name, + self.subscription_name) + if hasattr(self, "update_name"): + self.subscription_name = "test_subscription_update_2_%s" % ( + str(uuid.uuid4())[1:8]) + self.test_data['name'] = self.subscription_name + else: + self.test_data['name'] = self.subscription_name + self.test_data['id'] = self.subscription_id + + if not subscription_name: + raise Exception("Could not find the subscription to update.") + + if self.is_positive_test: + if hasattr(self, "wrong_subscription_id"): + self.subscription_id = 9999 + if hasattr(self, "plid_none"): + self.subscription_id = '' + response = self.update_subscription(self.test_data) + else: + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + if hasattr(self, "wrong_subscription_id"): + self.subscription_id = 9999 + response = self.update_subscription(self.test_data) + + self.assertEqual(response.status_code, + self.expected_data["status_code"]) + + def tearDown(self): + + # Disconnect the database + subscription_utils.delete_subscription(self.server, self.db_name, + self.subscription_name) + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_sql.py b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_sql.py new file mode 100644 index 000000000..797df3323 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/test_subscription_sql.py @@ -0,0 +1,88 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +from unittest.mock import patch +from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \ + import utils as tables_utils +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +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 . import utils as subscription_utils + + +class PublicationGetTestCase(BaseTestGenerator): + """This class will fetch the subscription under table node.""" + scenarios = utils.generate_scenarios('sql_subscription', + subscription_utils.test_cases) + + def setUp(self): + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + self.server_version = schema_info["server_version"] + if self.server_version < 99999: + self.skipTest( + "Logical replication is not supported " + "for server version less than 10" + + ) + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception( + "Could not connect to database to delete subscription.") + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to delete " + "subscription.") + self.table_name = "table_column_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = tables_utils.create_table(self.server, self.db_name, + self.schema_name, + self.table_name) + self.subscription_name = "test_subscription_delete_%s" % ( + str(uuid.uuid4())[1:8]) + self.subscription_id = \ + subscription_utils.create_subscription(self.server, self.db_name, + self.subscription_name) + + def get_sql(self): + return self.tester.get( + self.url + str(utils.SERVER_GROUP) + '/' + str( + self.server_id) + '/' + + str(self.db_id) + '/' + str(self.subscription_id), + content_type='html/json') + + def runTest(self): + """This function will fetch the subscription under table node.""" + + if self.is_positive_test: + response = self.get_sql() + else: + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + response = self.get_sql() + + self.assertEqual(response.status_code, + self.expected_data["status_code"]) + + def tearDown(self): + subscription_utils.delete_subscription(self.server, self.db_name, + self.subscription_name) + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/utils.py b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/utils.py new file mode 100644 index 000000000..385d7c73e --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/tests/utils.py @@ -0,0 +1,157 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + + +import sys +import os +import json +import traceback + +from regression.python_test_utils import test_utils as utils + +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/subscription_test_data.json") as data_file: + test_cases = json.load(data_file) + + +def get_tables(self): + tables = self.tester.get( + '/browser/subscription/get_tables/' + str( + utils.SERVER_GROUP) + '/' + str( + self.server_id) + '/' + + str(self.db_id) + '/', + content_type='html/json') + return json.dumps([tables.json['data'][1]['value']]) + + +def create_subscription_api(self): + return self.tester.post( + self.url + str(utils.SERVER_GROUP) + '/' + + str(self.server_id) + '/' + str( + self.db_id) + '/', + data=json.dumps(self.test_data), + content_type='html/json') + + +def create_subscription(server, db_name, subscription_name): + """ + This function creates a subscription. + :param server: server details + :type server: dict + :param db_name: database name + :type db_name: str + :param subscription_name: subscription name + :type subscription_name: str + :return subscription_id: subscription id + :rtype: int + """ + try: + connection = utils.get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode']) + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + query = """CREATE SUBSCRIPTION "%s" """ \ + """CONNECTION 'host=192.168.1.50 port=5432 user=foo """ \ + """dbname=foodb' """ \ + """PUBLICATION insert_only WITH (create_slot = false, """ \ + """enabled = false, slot_name=NONE, connect=false);""" % ( + subscription_name) + pg_cursor.execute(query) + connection.set_isolation_level(old_isolation_level) + connection.commit() + # Get role oid of newly added subscription + pg_cursor.execute("select oid from pg_subscription sub where " + "sub.subname='%s'" % + subscription_name) + subscription = pg_cursor.fetchone() + subscription_id = '' + if subscription: + subscription_id = subscription[0] + connection.close() + return subscription_id + except Exception: + traceback.print_exc(file=sys.stderr) + raise + + +def verify_subscription(server, db_name, subscription_name): + """ + This function verifies subscription exist in database or not. + :param server: server details + :type server: dict + :param db_name: database name + :type db_name: str + :param subscription_name: subscription name + :type subscription_name: str + :return subscription: subscription record from database + :rtype: tuple + """ + try: + connection = utils.get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode']) + pg_cursor = connection.cursor() + pg_cursor.execute("select * from pg_subscription sub " + "where sub.subname='%s'" % + subscription_name) + subscription = pg_cursor.fetchone() + connection.close() + return subscription + except Exception: + traceback.print_exc(file=sys.stderr) + raise + + +def delete_subscription(server, db_name, subscription_name): + """ + This function use to delete the existing subscription + + :param db_name: db_name + :type db_name: db_name object + :param server: server + :type server: server object + :param subscription_name: subscription name + :type subscription_name: str + :return: None + """ + + try: + connection = utils.get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode']) + pg_cursor = connection.cursor() + + pg_cursor.execute("select * from pg_subscription sub where " + "sub.subname='%s'" % + subscription_name) + subscription_count = pg_cursor.fetchone() + if subscription_count: + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + query = "DROP subscription %s" % \ + (subscription_name) + pg_cursor.execute(query) + connection.set_isolation_level(old_isolation_level) + connection.commit() + connection.close() + except Exception: + traceback.print_exc(file=sys.stderr) + raise diff --git a/web/pgadmin/tools/search_objects/templates/search_objects/sql/pg/10_plus/search.sql b/web/pgadmin/tools/search_objects/templates/search_objects/sql/pg/10_plus/search.sql index ac215eefd..3828d9068 100644 --- a/web/pgadmin/tools/search_objects/templates/search_objects/sql/pg/10_plus/search.sql +++ b/web/pgadmin/tools/search_objects/templates/search_objects/sql/pg/10_plus/search.sql @@ -320,6 +320,24 @@ FROM ( {% if all_obj %} UNION {% endif %} + +{% if all_obj or obj_type in ['publication'] %} + SELECT 'publication'::text AS obj_type, pubname AS obj_name, ':publication.'||pub.oid||':/' || pubname AS obj_path, ''::text AS schema_name, + {{ show_node_prefs['publication'] }} AS show_node, NULL AS other_info + FROM pg_publication pub +{% endif %} +{% if all_obj %} + UNION +{% endif %} + +{% if all_obj or obj_type in ['subscription'] %} + SELECT 'subscription'::text AS obj_type, subname AS obj_name, ':subscription.'||pub.oid||':/' || subname AS obj_path, ''::text AS schema_name, + {{ show_node_prefs['subscription'] }} AS show_node, NULL AS other_info + FROM pg_subscription pub +{% endif %} +{% if all_obj %} + UNION +{% endif %} {% if all_obj or obj_type in ['language'] %} SELECT 'language'::text AS obj_type, lanname AS obj_name, ':language.'||lan.oid||':/' || lanname AS obj_path, ''::text AS schema_name, {{ show_node_prefs['language'] }} AS show_node, NULL AS other_info diff --git a/web/pgadmin/tools/search_objects/templates/search_objects/sql/pg/11_plus/search.sql b/web/pgadmin/tools/search_objects/templates/search_objects/sql/pg/11_plus/search.sql index 4e07ff079..e8631f136 100644 --- a/web/pgadmin/tools/search_objects/templates/search_objects/sql/pg/11_plus/search.sql +++ b/web/pgadmin/tools/search_objects/templates/search_objects/sql/pg/11_plus/search.sql @@ -337,6 +337,24 @@ FROM ( {% if all_obj %} UNION {% endif %} + +{% if all_obj or obj_type in ['publication'] %} + SELECT 'publication'::text AS obj_type, pubname AS obj_name, ':publication.'||pub.oid||':/' || pubname AS obj_path, ''::text AS schema_name, + {{ show_node_prefs['publication'] }} AS show_node, NULL AS other_info + FROM pg_publication pub +{% endif %} +{% if all_obj %} + UNION +{% endif %} + +{% if all_obj or obj_type in ['subscription'] %} + SELECT 'subscription'::text AS obj_type, subname AS obj_name, ':subscription.'||pub.oid||':/' || subname AS obj_path, ''::text AS schema_name, + {{ show_node_prefs['subscription'] }} AS show_node, NULL AS other_info + FROM pg_subscription pub +{% endif %} +{% if all_obj %} + UNION +{% endif %} {% if all_obj or obj_type in ['language'] %} SELECT 'language'::text AS obj_type, lanname AS obj_name, ':language.'||lan.oid||':/' || lanname AS obj_path, ''::text AS schema_name, {{ show_node_prefs['language'] }} AS show_node, NULL AS other_info diff --git a/web/pgadmin/tools/search_objects/templates/search_objects/sql/ppas/10_plus/search.sql b/web/pgadmin/tools/search_objects/templates/search_objects/sql/ppas/10_plus/search.sql index a41a375eb..4ed715463 100644 --- a/web/pgadmin/tools/search_objects/templates/search_objects/sql/ppas/10_plus/search.sql +++ b/web/pgadmin/tools/search_objects/templates/search_objects/sql/ppas/10_plus/search.sql @@ -361,6 +361,25 @@ FROM ( {% if all_obj %} UNION {% endif %} + +{% if all_obj or obj_type in ['publication'] %} + SELECT 'publication'::text AS obj_type, pubname AS obj_name, ':publication.'||pub.oid||':/' || pubname AS obj_path, ''::text AS schema_name, + {{ show_node_prefs['publication'] }} AS show_node, NULL AS other_info + FROM pg_publication pub +{% endif %} +{% if all_obj %} + UNION +{% endif %} + +{% if all_obj or obj_type in ['subscription'] %} + SELECT 'subscription'::text AS obj_type, subname AS obj_name, ':subscription.'||pub.oid||':/' || subname AS obj_path, ''::text AS schema_name, + {{ show_node_prefs['subscription'] }} AS show_node, NULL AS other_info + FROM pg_subscription pub +{% endif %} +{% if all_obj %} + UNION +{% endif %} + {% if all_obj or obj_type in ['language'] %} SELECT 'language'::text AS obj_type, lanname AS obj_name, ':language.'||lan.oid||':/' || lanname AS obj_path, ''::text AS schema_name, {{ show_node_prefs['language'] }} AS show_node, NULL AS other_info diff --git a/web/pgadmin/tools/search_objects/templates/search_objects/sql/ppas/12_plus/search.sql b/web/pgadmin/tools/search_objects/templates/search_objects/sql/ppas/12_plus/search.sql index 796fc30b8..3633974ec 100644 --- a/web/pgadmin/tools/search_objects/templates/search_objects/sql/ppas/12_plus/search.sql +++ b/web/pgadmin/tools/search_objects/templates/search_objects/sql/ppas/12_plus/search.sql @@ -368,6 +368,25 @@ FROM ( {% if all_obj %} UNION {% endif %} + +{% if all_obj or obj_type in ['publication'] %} + SELECT 'publication'::text AS obj_type, pubname AS obj_name, ':publication.'||pub.oid||':/' || pubname AS obj_path, ''::text AS schema_name, + {{ show_node_prefs['publication'] }} AS show_node, NULL AS other_info + FROM pg_publication pub +{% endif %} +{% if all_obj %} + UNION +{% endif %} + +{% if all_obj or obj_type in ['subscription'] %} + SELECT 'subscription'::text AS obj_type, subname AS obj_name, ':subscription.'||pub.oid||':/' || subname AS obj_path, ''::text AS schema_name, + {{ show_node_prefs['subscription'] }} AS show_node, NULL AS other_info + FROM pg_subscription pub +{% endif %} +{% if all_obj %} + UNION +{% endif %} + {% if all_obj or obj_type in ['language'] %} SELECT 'language'::text AS obj_type, lanname AS obj_name, ':language.'||lan.oid||':/' || lanname AS obj_path, ''::text AS schema_name, {{ show_node_prefs['language'] }} AS show_node, NULL AS other_info diff --git a/web/pgadmin/tools/search_objects/utils.py b/web/pgadmin/tools/search_objects/utils.py index ce5ece28d..a7d556853 100644 --- a/web/pgadmin/tools/search_objects/utils.py +++ b/web/pgadmin/tools/search_objects/utils.py @@ -43,7 +43,8 @@ class SearchObjectsHelper: 'trigger_function', 'fts_template', 'collation', 'view', 'mview', 'fts_configuration', 'extension', 'language', 'event_trigger', 'foreign_server', 'user_mapping', - 'foreign_data_wrapper', 'row_security_policy' + 'foreign_data_wrapper', 'row_security_policy', + 'publication', 'subscription' ] if node_types is None else node_types @property diff --git a/web/webpack.config.js b/web/webpack.config.js index 96ef708ab..7f210955e 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -432,6 +432,8 @@ module.exports = [{ ',pgadmin.node.database' + ',pgadmin.node.role' + ',pgadmin.node.cast' + + ',pgadmin.node.publication' + + ',pgadmin.node.subscription' + ',pgadmin.node.tablespace' + ',pgadmin.node.resource_group' + ',pgadmin.node.event_trigger' + diff --git a/web/webpack.shim.js b/web/webpack.shim.js index 6fc26021e..259348533 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -202,6 +202,8 @@ var webpackShimConfig = { 'pgadmin.help': path.join(__dirname, './pgadmin/help/static/js/help'), 'pgadmin.misc.explain': path.join(__dirname, './pgadmin/misc/static/explain/js/explain'), 'pgadmin.node.cast': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/casts/static/js/cast'), + 'pgadmin.node.publication': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/publications/static/js/publication'), + 'pgadmin.node.subscription': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription'), 'pgadmin.node.catalog': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/catalog'), 'pgadmin.node.catalog_object': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/catalog_objects/static/js/catalog_object'), 'pgadmin.node.catalog_object_column': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/catalog_objects/columns/static/js/catalog_object_column'),