public inbox for [email protected]
help / color / mirror / Atom feedFrom: Pradip Parkale <[email protected]>
To: pgadmin-hackers <[email protected]>
Subject: [pgAdmin][RM5912]: Added support for Logical Replication.
Date: Mon, 11 Jan 2021 17:07:05 +0530
Message-ID: <CAJ9T6Su-iFZ1LikdjUAXEnTXYg8wBZrR23iWYfeGk2pLOVcqsg@mail.gmail.com> (raw)
Hi Hackers,
Please find the attached patch for logical replication support.
--
Thanks & Regards,
Pradip Parkale
Software Engineer | EnterpriseDB Corporation
Attachments:
[application/octet-stream] RM5912.patch (223.4K, 3-RM5912.patch)
download | inline diff:
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#DDE1F0;stroke:#4B5FAD;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+ .st1{fill:#4B5FAD;}
+ .st2{fill:none;stroke:#4B5FAD;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+</style>
+<g>
+ <path class="st0" d="M10,13c-2,2-5.1,2-7.1,0s-2-5.1,0-7.1C4.5,7.5,8.5,11.5,10,13z"/>
+</g>
+<g>
+ <path class="st0" d="M11.8,11.3c-2,2-5.1,2-7.1,0s-2-5.1,0-7.1C6.2,5.7,10.2,9.8,11.8,11.3z"/>
+</g>
+<path class="st1" d="M8.7,7.8l0.8-0.7c0.1,0.1,0.3,0.1,0.4,0.1c0.6,0,1-0.4,1-1s-0.4-1-1-1s-1,0.4-1,1c0,0.1,0,0.3,0.1,0.4L8.2,7.3
+ L8.7,7.8z"/>
+<g>
+ <path class="st2" d="M10.5,1.6c2.2,0,4,1.8,4,4"/>
+ <path class="st2" d="M10.6,3.6c1.1,0,2,0.9,2,2"/>
+</g>
+</svg>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#FBEEDF;stroke:#E58E26;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+ .st1{fill:#E58E26;}
+ .st2{fill:none;stroke:#E58E26;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+</style>
+<g>
+ <g>
+ <g>
+ <path class="st0" d="M12.9,5.7c2,2,2,5.1,0,7.1s-5.1,2-7.1,0C7.3,11.3,11.4,7.2,12.9,5.7z"/>
+ </g>
+ </g>
+ <g>
+ <g>
+ <path class="st0" d="M11.4,4.2c2,2,2,5.1,0,7.1s-5.1,2-7.1,0C5.8,9.8,9.9,5.7,11.4,4.2z"/>
+ </g>
+ <g>
+ <path class="st1" d="M7.8,7.3L7.1,6.6c0.1-0.1,0.1-0.3,0.1-0.4c0-0.6-0.4-1-1-1s-1,0.4-1,1s0.4,1,1,1c0.1,0,0.3,0,0.4-0.1
+ l0.7,0.7"/>
+ <g>
+ <path class="st2" d="M5.6,1.7c0,2.2-1.8,4-4,4"/>
+ <path class="st2" d="M3.7,1.8c0,1.1-0.9,2-2,2"/>
+ </g>
+ </g>
+ </g>
+</g>
+</svg>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#DDE1F0;stroke:#4B5FAD;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+ .st1{fill:#4B5FAD;}
+ .st2{fill:none;stroke:#4B5FAD;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+</style>
+<g>
+ <g>
+ <path class="st0" d="M11,12c-2,2-5.1,2-7.1,0s-2-5.1,0-7.1C5.4,6.4,9.5,10.5,11,12z"/>
+ </g>
+ <g>
+ <path class="st1" d="M7.9,8.6l0.7-0.7C8.7,8,8.9,8,9,8c0.6,0,1-0.4,1-1S9.6,6,9,6S8,6.4,8,7c0,0.1,0,0.3,0.1,0.4L7.4,8.1"/>
+ <g>
+ <path class="st2" d="M9.6,2.5c2.2,0,4,1.8,4,4"/>
+ <path class="st2" d="M9.7,4.4c1.1,0,2,0.9,2,2"/>
+ </g>
+ </g>
+</g>
+</svg>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#FBEEDF;stroke:#E58E26;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+ .st1{fill:#E58E26;}
+ .st2{fill:none;stroke:#E58E26;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+</style>
+<g>
+ <g>
+ <path class="st0" d="M12.2,5c2,2,2,5.1,0,7.1s-5.1,2-7.1,0C6.6,10.6,10.7,6.5,12.2,5z"/>
+ </g>
+ <g>
+ <path class="st1" d="M8.6,8.1L7.9,7.4C8,7.3,8,7.1,8,7c0-0.6-0.4-1-1-1S6,6.4,6,7s0.4,1,1,1c0.1,0,0.3,0,0.4-0.1l0.7,0.7"/>
+ <g>
+ <path class="st2" d="M6.4,2.5c0,2.2-1.8,4-4,4"/>
+ <path class="st2" d="M4.5,2.6c0,1.1-0.9,2-2,2"/>
+ </g>
+ </g>
+</g>
+</svg>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#DDE1F0;stroke:#4B5FAD;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+ .st1{fill:#4B5FAD;}
+ .st2{fill:none;stroke:#4B5FAD;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+</style>
+<g>
+ <path class="st0" d="M10,13c-2,2-5.1,2-7.1,0s-2-5.1,0-7.1C4.5,7.5,8.5,11.5,10,13z"/>
+</g>
+<g>
+ <path class="st0" d="M11.8,11.3c-2,2-5.1,2-7.1,0s-2-5.1,0-7.1C6.2,5.7,10.2,9.8,11.8,11.3z"/>
+</g>
+<path class="st1" d="M8.7,7.8l0.8-0.7c0.1,0.1,0.3,0.1,0.4,0.1c0.6,0,1-0.4,1-1s-0.4-1-1-1s-1,0.4-1,1c0,0.1,0,0.3,0.1,0.4L8.2,7.3
+ L8.7,7.8z"/>
+<g>
+ <path class="st2" d="M10.5,1.6c2.2,0,4,1.8,4,4"/>
+ <path class="st2" d="M10.6,3.6c1.1,0,2,0.9,2,2"/>
+</g>
+</svg>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#FBEEDF;stroke:#E58E26;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+ .st1{fill:#E58E26;}
+ .st2{fill:none;stroke:#E58E26;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+</style>
+<g>
+ <g>
+ <g>
+ <path class="st0" d="M12.9,5.7c2,2,2,5.1,0,7.1s-5.1,2-7.1,0C7.3,11.3,11.4,7.2,12.9,5.7z"/>
+ </g>
+ </g>
+ <g>
+ <g>
+ <path class="st0" d="M11.4,4.2c2,2,2,5.1,0,7.1s-5.1,2-7.1,0C5.8,9.8,9.9,5.7,11.4,4.2z"/>
+ </g>
+ <g>
+ <path class="st1" d="M7.8,7.3L7.1,6.6c0.1-0.1,0.1-0.3,0.1-0.4c0-0.6-0.4-1-1-1s-1,0.4-1,1s0.4,1,1,1c0.1,0,0.3,0,0.4-0.1
+ l0.7,0.7"/>
+ <g>
+ <path class="st2" d="M5.6,1.7c0,2.2-1.8,4-4,4"/>
+ <path class="st2" d="M3.7,1.8c0,1.1-0.9,2-2,2"/>
+ </g>
+ </g>
+ </g>
+</g>
+</svg>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#DDE1F0;stroke:#4B5FAD;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+ .st1{fill:#4B5FAD;}
+ .st2{fill:none;stroke:#4B5FAD;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+</style>
+<g>
+ <g>
+ <path class="st0" d="M11,12c-2,2-5.1,2-7.1,0s-2-5.1,0-7.1C5.4,6.4,9.5,10.5,11,12z"/>
+ </g>
+ <g>
+ <path class="st1" d="M7.9,8.6l0.7-0.7C8.7,8,8.9,8,9,8c0.6,0,1-0.4,1-1S9.6,6,9,6S8,6.4,8,7c0,0.1,0,0.3,0.1,0.4L7.4,8.1"/>
+ <g>
+ <path class="st2" d="M9.6,2.5c2.2,0,4,1.8,4,4"/>
+ <path class="st2" d="M9.7,4.4c1.1,0,2,0.9,2,2"/>
+ </g>
+ </g>
+</g>
+</svg>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#FBEEDF;stroke:#E58E26;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+ .st1{fill:#E58E26;}
+ .st2{fill:none;stroke:#E58E26;stroke-width:0.65;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
+</style>
+<g>
+ <g>
+ <path class="st0" d="M12.2,5c2,2,2,5.1,0,7.1s-5.1,2-7.1,0C6.6,10.6,10.7,6.5,12.2,5z"/>
+ </g>
+ <g>
+ <path class="st1" d="M8.6,8.1L7.9,7.4C8,7.3,8,7.1,8,7c0-0.6-0.4-1-1-1S6,6.4,6,7s0.4,1,1,1c0.1,0,0.3,0,0.4-0.1l0.7,0.7"/>
+ <g>
+ <path class="st2" d="M6.4,2.5c0,2.2-1.8,4-4,4"/>
+ <path class="st2" d="M4.5,2.6c0,1.1-0.9,2-2,2"/>
+ </g>
+ </g>
+</g>
+</svg>
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([
+ '<label class="<%=Backform.controlLabelClassName%>" for="<%=cId%>"><%=label%></label>',
+ '<div class="<%=Backform.controlsClassName%>">',
+ '<div class="input-group">',
+ ' <select title="<%=name%>" id="<%=cId%>" class="<%=Backform.controlClassName%> <%=extraClasses.join(\' \')%>"',
+ ' name="<%=name%>" value="<%-value%>" <%=disabled ? "disabled" : ""%> <%=readonly ? "disabled" : ""%>',
+ ' <%=required ? "required" : ""%><%= select2.multiple ? " multiple>" : ">" %>',
+ ' <%=select2.first_empty ? " <option></option>" : ""%>',
+ ' <% for (var i=0; i < options.length; i++) {%>',
+ ' <% var option = options[i]; %>',
+ ' <option ',
+ ' <% if (option.image) { %> data-image=<%=option.image%> <%}%>',
+ ' value=<%- formatter.fromRaw(option.value) %>',
+ ' <% if (option.selected) {%>selected="selected"<%} else {%>',
+ ' <% if (!select2.multiple && option.value === rawValue) {%>selected="selected"<%}%>',
+ ' <% if (select2.multiple && rawValue && rawValue.indexOf(option.value) != -1){%>selected="selected" data-index="rawValue.indexOf(option.value)"<%}%>',
+ ' <%}%>',
+ ' <%= disabled ? "disabled" : ""%> <%=readonly ? "disabled" : ""%>><%-option.label%></option>',
+ ' <%}%>',
+ ' </select>',
+ '<div class="input-group-append">',
+ '<button class="btn btn-primary-icon fa fa-sync get_publication" <%=disabled ? "disabled" : ""%> <%=readonly ? "disabled" : ""%> aria-hidden="true" aria-label="' + gettext('Get Publication') + '" title="' + gettext('Get Publication') + '"></button>',
+ '</div>',
+ '</div>',
+ '<% if (helpMessage && helpMessage.length) { %>',
+ '<span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
+ '<% } %>',
+ '</div>',
+ ].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'),
view thread (7+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected]
Subject: Re: [pgAdmin][RM5912]: Added support for Logical Replication.
In-Reply-To: <CAJ9T6Su-iFZ1LikdjUAXEnTXYg8wBZrR23iWYfeGk2pLOVcqsg@mail.gmail.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox