diff --git a/docs/en_US/dbms_job.rst b/docs/en_US/dbms_job.rst
new file mode 100644
index 000000000..8254e03a7
--- /dev/null
+++ b/docs/en_US/dbms_job.rst
@@ -0,0 +1,108 @@
+.. _dbms_job:
+
+*****************
+`DBMS Job`:index:
+*****************
+
+Use the *DBMS Job* dialog to create a DBMS Job.
+
+.. image:: images/dbms_job_general.png
+ :alt: DBMS Job dialog general tab
+ :align: center
+
+Use the fields in the *General* tab to create job:
+
+* Use the *Name* field to add a descriptive name for the job. The name will
+ be displayed in the *pgAdmin* object explorer.
+* Use the *Enabled?* switch to indicate that job should be enabled or disabled.
+* Use the *Job Type* field to select the type of the job. Type could be SELF-CONTAINED or PRE-DEFINED.
+ If the Job Type is Self-Contained you need to specify the action and repeat interval in the Action and Repeat tabs respectively.
+ If the Job Type is Pre-Defined you need to specify the existing Program and Schedule names in the Pre-Defined tab.
+* Store notes about the job in the *Comment* field.
+
+Click the *Action* tab to continue.
+
+.. image:: images/dbms_job_action.png
+ :alt: DBMS Job dialog action tab
+ :align: center
+
+Use the *Action* tab to select the action for the job. This tab is only enabled when the job type is 'SELF-CONTAINED'.
+
+* Use the *Type* field to select the type of the job. Type could be PLSQL BLOCK or STORED PROCEDURE.
+* Use the *Procedure* field to select an existing procedure that executes when the job is invoked.
+* *Number of Arguments* field is read-only and indicates the quantity of arguments necessary for the chosen procedure.
+
+Click the *Code* tab to continue.
+
+.. image:: images/dbms_job_code.png
+ :alt: DBMS Job dialog code tab
+ :align: center
+
+* Use the *Code* field to write the code that executes when the job is invoked.
+ This tab is only enabled when the job type is 'SELF-CONTAINED' and type of the action is set to 'PLSQL BLOCK'.
+
+
+Click the *Arguments* tab to continue.
+
+.. image:: images/dbms_job_arguments.png
+ :alt: DBMS Job dialog arguments tab
+ :align: center
+
+* *Arguments* tab outlines the arguments required by the selected procedure in the 'Action' tab. This tab is only enabled when the job type is 'SELF-CONTAINED'.
+
+
+Click the *Repeat* tab to continue.
+
+.. image:: images/dbms_job_repeat.png
+ :alt: DBMS Job dialog repeat tab
+ :align: center
+
+Use the *Repeat* tab to select the repeat interval for the job. This tab is only enabled when the job type is 'SELF-CONTAINED'.
+
+* Use the calendar selector in the *Start* field to specify the starting date
+ and time for the job.
+* Use the calendar selector in the *End* field to specify the ending date and
+ time for the job.
+* Use the *Frequency* field to select the frequency. Frequency is one of the following:
+ YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY.
+* Use the *Date* field to select the date on which job will execute.Date is YYYYMMDD.
+* Use the *Months* field to select the months in which the job will execute.
+* Use the *Week Days* field to select the days on which the job will execute.
+* Use the *Month Days* field to select the numeric days on which the job will
+ execute.
+* Use the *Hours* field to select the hour at which the job will execute.
+* Use the *Minutes* field to select the minute at which the job will execute.
+
+Click the *Pre-Defined* tab to continue.
+
+.. image:: images/dbms_job_predefined.png
+ :alt: DBMS Job dialog predefined tab
+ :align: center
+
+Use the *Pre-Defined* tab to select the existing program and schedule to create the job.
+This tab is only enabled when the job type is 'PRE-DEFINED'.
+
+* Use the *Program Name* field to select the existing program.
+* Use the *Schedule Name* field to select an existing schedule.
+
+
+Click the *SQL* tab to continue.
+
+Your entries in the *DBMS Job* dialog generate a SQL command (see an example below).
+Use the *SQL* tab for review; revisit or switch tabs to make any changes to the
+SQL command.
+
+**Example**
+
+The following is an example of the sql command generated by user selections in
+the *DBMS Job* dialog:
+
+.. image:: images/dbms_job_sql.png
+ :alt: DBMS Job dialog sql tab
+ :align: center
+
+* Click the *Info* button (i) to access online help.
+* Click the *Help* button (?) to access dialog help.
+* Click the *Save* button to save work.
+* Click the *Close* button to exit without saving work.
+* Click the *Reset* button to restore configuration parameters.
\ No newline at end of file
diff --git a/docs/en_US/dbms_job_scheduler.rst b/docs/en_US/dbms_job_scheduler.rst
new file mode 100644
index 000000000..b5022fb7c
--- /dev/null
+++ b/docs/en_US/dbms_job_scheduler.rst
@@ -0,0 +1,45 @@
+.. _dbms_job_scheduler:
+
+********************************
+`Using EDB Job Scheduler`:index:
+********************************
+
+In the past versions of EPAS, DBMS_SCHEDULER or DBMS_JOBS required the configuration
+of pgAgent, an essential service for their functionality. Maintaining pgAgent in a
+production environment is cumbersome. The need for correct configuration, regular updates,
+and ensuring the service’s health added complexity.
+
+EPAS 16 revolutionizes job scheduling by eliminating the need for the pgAgent component.
+The new version introduces EDB Job Scheduler which is an extension that runs the job scheduler
+as a background process for the DBMS_SCHEDULER and DBMS_JOB packages.
+
+The EDB Job Scheduler has a scheduler process that starts when the database cluster starts.
+To start the scheduler process, load the EDB Job Scheduler extension using the **shared_preload_libraries**
+parameter. After you load the extension, create the extension using the CREATE EXTENSION command.
+The database in which you're creating the extension must be listed in the **edb_job_scheduler.database_list**
+parameter.
+
+Instructions for configuring the EDB Job Scheduler can be found in the
+`Configuring EDB Job Scheduler `_.
+
+.. image:: images/dbms_job_scheduler.png
+ :alt: DBMS Job Scheduler Object Browser
+ :align: center
+
+Check the status of all the jobs
+********************************
+
+To check the running status of all the jobs select the 'DBMS Job Scheduler' collection node from the object
+explorer and select the Properties tab.
+
+.. image:: images/dbms_job_details.png
+ :alt: DBMS Job Details
+ :align: center
+
+
+.. toctree::
+ :maxdepth: 1
+
+ dbms_job
+ dbms_program
+ dbms_schedule
\ No newline at end of file
diff --git a/docs/en_US/dbms_program.rst b/docs/en_US/dbms_program.rst
new file mode 100644
index 000000000..0c0aa85f4
--- /dev/null
+++ b/docs/en_US/dbms_program.rst
@@ -0,0 +1,71 @@
+.. _dbms_program:
+
+*********************
+`DBMS Program`:index:
+*********************
+
+Use the *DBMS Program* dialog to create a DBMS Program.
+
+.. image:: images/dbms_program_general.png
+ :alt: DBMS Program dialog general tab
+ :align: center
+
+Use the fields in the *General* tab to create program:
+
+* Use the *Name* field to add a descriptive name for the program. The name will
+ be displayed in the *pgAdmin* object explorer.
+* Use the *Enabled?* switch to indicate that program should be enabled or disabled.
+* Store notes about the program in the *Comment* field.
+
+Click the *Action* tab to continue.
+
+.. image:: images/dbms_program_action.png
+ :alt: DBMS Program dialog action tab
+ :align: center
+
+Use the *Action* tab to select the action for the program:
+
+* Use the *Type* field to select the type of the program. Type could be PLSQL BLOCK or STORED PROCEDURE.
+* Use the *Procedure* field to select an existing procedure that executes when the program is invoked.
+* *Number of Arguments* field is read-only and indicates the quantity of arguments necessary for the chosen procedure.
+
+Click the *Code* tab to continue.
+
+.. image:: images/dbms_program_code.png
+ :alt: DBMS Program dialog code tab
+ :align: center
+
+* Use the *Code* field to write the code that executes when the program is invoked.
+ This tab is only enabled when the type of the program is set to 'PLSQL BLOCK'.
+
+
+Click the *Arguments* tab to continue.
+
+.. image:: images/dbms_program_arguments.png
+ :alt: DBMS Program dialog arguments tab
+ :align: center
+
+* *Arguments* tab is a read-only section that outlines the arguments required by the selected procedure in the 'Action' tab.
+
+
+Click the *SQL* tab to continue.
+
+Your entries in the *DBMS Program* dialog generate a SQL command (see an example below).
+Use the *SQL* tab for review; revisit or switch tabs to make any changes to the
+SQL command.
+
+**Example**
+
+The following is an example of the sql command generated by user selections in
+the *DBMS Program* dialog:
+
+.. image:: images/dbms_program_sql.png
+ :alt: DBMS Program dialog sql tab
+ :align: center
+
+* Click the *Info* button (i) to access online help.
+* Click the *Help* button (?) to access dialog help.
+* Click the *Save* button to save work.
+* Click the *Close* button to exit without saving work.
+* Click the *Reset* button to restore configuration parameters.
+
diff --git a/docs/en_US/dbms_schedule.rst b/docs/en_US/dbms_schedule.rst
new file mode 100644
index 000000000..9811267ec
--- /dev/null
+++ b/docs/en_US/dbms_schedule.rst
@@ -0,0 +1,61 @@
+.. _dbms_schedule:
+
+**********************
+`DBMS Schedule`:index:
+**********************
+
+Use the *DBMS Schedule* dialog to create a DBMS Schedule.
+
+.. image:: images/dbms_schedule_general.png
+ :alt: DBMS Schedule dialog general tab
+ :align: center
+
+Use the fields in the *General* tab to create schedule:
+
+* Use the *Name* field to add a descriptive name for the schedule. The name will
+ be displayed in the *pgAdmin* object explorer.
+* Store notes about the schedule in the *Comment* field.
+
+Click the *Repeat* tab to continue.
+
+.. image:: images/dbms_schedule_repeat.png
+ :alt: DBMS Schedule dialog repeat tab
+ :align: center
+
+Use the *Repeat* tab to select the repeat interval for the schedule:
+
+* Use the calendar selector in the *Start* field to specify the starting date
+ and time for the schedule.
+* Use the calendar selector in the *End* field to specify the ending date and
+ time for the schedule.
+* Use the *Frequency* field to select the frequency. Frequency is one of the following:
+ YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY.
+* Use the *Date* field to select the date on which schedule will execute.Date is YYYYMMDD.
+* Use the *Months* field to select the months in which the schedule will execute.
+* Use the *Week Days* field to select the days on which the schedule will execute.
+* Use the *Month Days* field to select the numeric days on which the schedule will
+ execute.
+* Use the *Hours* field to select the hour at which the schedule will execute.
+* Use the *Minutes* field to select the minute at which the schedule will execute.
+
+
+Click the *SQL* tab to continue.
+
+Your entries in the *DBMS Schedule* dialog generate a SQL command (see an example below).
+Use the *SQL* tab for review; revisit or switch tabs to make any changes to the
+SQL command.
+
+**Example**
+
+The following is an example of the sql command generated by user selections in
+the *DBMS Schedule* dialog:
+
+.. image:: images/dbms_schedule_sql.png
+ :alt: DBMS Schedule dialog sql tab
+ :align: center
+
+* Click the *Info* button (i) to access online help.
+* Click the *Help* button (?) to access dialog help.
+* Click the *Save* button to save work.
+* Click the *Close* button to exit without saving work.
+* Click the *Reset* button to restore configuration parameters.
\ No newline at end of file
diff --git a/docs/en_US/images/dbms_job_action.png b/docs/en_US/images/dbms_job_action.png
new file mode 100644
index 000000000..6e7345eb1
Binary files /dev/null and b/docs/en_US/images/dbms_job_action.png differ
diff --git a/docs/en_US/images/dbms_job_arguments.png b/docs/en_US/images/dbms_job_arguments.png
new file mode 100644
index 000000000..eabad426f
Binary files /dev/null and b/docs/en_US/images/dbms_job_arguments.png differ
diff --git a/docs/en_US/images/dbms_job_code.png b/docs/en_US/images/dbms_job_code.png
new file mode 100644
index 000000000..fe20dbeec
Binary files /dev/null and b/docs/en_US/images/dbms_job_code.png differ
diff --git a/docs/en_US/images/dbms_job_details.png b/docs/en_US/images/dbms_job_details.png
new file mode 100644
index 000000000..4818ab73a
Binary files /dev/null and b/docs/en_US/images/dbms_job_details.png differ
diff --git a/docs/en_US/images/dbms_job_general.png b/docs/en_US/images/dbms_job_general.png
new file mode 100644
index 000000000..3550a206f
Binary files /dev/null and b/docs/en_US/images/dbms_job_general.png differ
diff --git a/docs/en_US/images/dbms_job_predefined.png b/docs/en_US/images/dbms_job_predefined.png
new file mode 100644
index 000000000..9c7d67241
Binary files /dev/null and b/docs/en_US/images/dbms_job_predefined.png differ
diff --git a/docs/en_US/images/dbms_job_repeat.png b/docs/en_US/images/dbms_job_repeat.png
new file mode 100644
index 000000000..4abb69845
Binary files /dev/null and b/docs/en_US/images/dbms_job_repeat.png differ
diff --git a/docs/en_US/images/dbms_job_scheduler.png b/docs/en_US/images/dbms_job_scheduler.png
new file mode 100644
index 000000000..8b185d595
Binary files /dev/null and b/docs/en_US/images/dbms_job_scheduler.png differ
diff --git a/docs/en_US/images/dbms_job_sql.png b/docs/en_US/images/dbms_job_sql.png
new file mode 100644
index 000000000..ebcbfadef
Binary files /dev/null and b/docs/en_US/images/dbms_job_sql.png differ
diff --git a/docs/en_US/images/dbms_program_action.png b/docs/en_US/images/dbms_program_action.png
new file mode 100644
index 000000000..83a598dcc
Binary files /dev/null and b/docs/en_US/images/dbms_program_action.png differ
diff --git a/docs/en_US/images/dbms_program_arguments.png b/docs/en_US/images/dbms_program_arguments.png
new file mode 100644
index 000000000..c16e5da5b
Binary files /dev/null and b/docs/en_US/images/dbms_program_arguments.png differ
diff --git a/docs/en_US/images/dbms_program_code.png b/docs/en_US/images/dbms_program_code.png
new file mode 100644
index 000000000..dcaba6e45
Binary files /dev/null and b/docs/en_US/images/dbms_program_code.png differ
diff --git a/docs/en_US/images/dbms_program_general.png b/docs/en_US/images/dbms_program_general.png
new file mode 100644
index 000000000..8d452e551
Binary files /dev/null and b/docs/en_US/images/dbms_program_general.png differ
diff --git a/docs/en_US/images/dbms_program_sql.png b/docs/en_US/images/dbms_program_sql.png
new file mode 100644
index 000000000..7e8cce743
Binary files /dev/null and b/docs/en_US/images/dbms_program_sql.png differ
diff --git a/docs/en_US/images/dbms_schedule_general.png b/docs/en_US/images/dbms_schedule_general.png
new file mode 100644
index 000000000..51014d3b3
Binary files /dev/null and b/docs/en_US/images/dbms_schedule_general.png differ
diff --git a/docs/en_US/images/dbms_schedule_repeat.png b/docs/en_US/images/dbms_schedule_repeat.png
new file mode 100644
index 000000000..6438ab7f2
Binary files /dev/null and b/docs/en_US/images/dbms_schedule_repeat.png differ
diff --git a/docs/en_US/images/dbms_schedule_sql.png b/docs/en_US/images/dbms_schedule_sql.png
new file mode 100644
index 000000000..b7e81556e
Binary files /dev/null and b/docs/en_US/images/dbms_schedule_sql.png differ
diff --git a/docs/en_US/managing_database_objects.rst b/docs/en_US/managing_database_objects.rst
index 25200a7ab..5b2975685 100644
--- a/docs/en_US/managing_database_objects.rst
+++ b/docs/en_US/managing_database_objects.rst
@@ -18,6 +18,7 @@ node, and select *Create Cast...*
:maxdepth: 1
cast_dialog
+ dbms_job_scheduler
collation_dialog
domain_dialog
domain_constraint_dialog
diff --git a/web/pgadmin/browser/server_groups/servers/databases/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/__init__.py
index 268999bf3..35bb68711 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/__init__.py
@@ -128,6 +128,9 @@ class DatabaseModule(CollectionNodeModule):
from .subscriptions import blueprint as module
self.submodules.append(module)
+ from .dbms_job_scheduler import blueprint as module
+ self.submodules.append(module)
+
super().register(app, options)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/__init__.py
new file mode 100644
index 000000000..f7ece6d6e
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/__init__.py
@@ -0,0 +1,280 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+""" Implements DBMS Job Scheduler objects Node."""
+
+from functools import wraps
+from flask import render_template
+from flask_babel import gettext
+from pgadmin.browser.collection import CollectionNodeModule
+from pgadmin.browser.server_groups.servers import databases
+from pgadmin.browser.utils import PGChildNodeView
+from pgadmin.utils.ajax import (make_json_response, internal_server_error,
+ make_response as ajax_response)
+from pgadmin.utils.driver import get_driver
+from config import PG_DEFAULT_DRIVER
+from pgadmin.utils.constants import DBMS_JOB_SCHEDULER_ID
+
+
+class DBMSJobSchedulerModule(CollectionNodeModule):
+ """
+ class DBMSJobSchedulerModule(CollectionNodeModule)
+
+ A module class for DBMS Job Scheduler objects node derived
+ from CollectionNodeModule.
+
+ Methods:
+ -------
+ * __init__(*args, **kwargs)
+ - Method is used to initialize the DBMS Job Scheduler objects, and it's
+ base module.
+
+ * get_nodes(gid, sid, did, scid, coid)
+ - Method is used to generate the browser collection node.
+
+ * script_load()
+ - Load the module script for DBMS Job Scheduler objects, when any of the
+ server node is initialized.
+
+ * backend_supported(manager, **kwargs)
+
+ * registert(self, app, options)
+ - Override the default register function to automagically register
+ sub-modules at once.
+ """
+ _NODE_TYPE = 'dbms_job_scheduler'
+ _COLLECTION_LABEL = gettext("DBMS Job Scheduler")
+
+ def __init__(self, *args, **kwargs):
+ """
+ Method is used to initialize the DBMSJobSchedulerModule, and it's base
+ module.
+
+ Args:
+ *args:
+ **kwargs:
+ """
+ super().__init__(*args, **kwargs)
+ self.min_ver = None
+ self.max_ver = None
+
+ @property
+ def node_icon(self):
+ """
+ icon to be displayed for the browser nodes
+ """
+ return 'icon-coll-dbms_job_scheduler'
+
+ def get_nodes(self, gid, sid, did):
+ """
+ Generate the collection node
+ """
+ if self.show_node:
+ yield self.generate_browser_node(DBMS_JOB_SCHEDULER_ID, did,
+ self._COLLECTION_LABEL, None)
+
+ @property
+ def script_load(self):
+ """
+ Load the module script for server, when any of the database node is
+ initialized.
+ """
+ return databases.DatabaseModule.node_type
+
+ def backend_supported(self, manager, **kwargs):
+ """
+ Function is used to check the pre-requisite for this node.
+ Args:
+ manager:
+ **kwargs:
+
+ Returns:
+
+ """
+ if hasattr(self, 'show_node') and not self.show_node:
+ return False
+
+ # Get the connection for the respective database
+ conn = manager.connection(did=kwargs['did'])
+ # Checking whether both 'edb_job_scheduler' and 'dbms_scheduler'
+ # extensions are created or not.
+ status, res = conn.execute_scalar("""
+ SELECT COUNT(*) FROM pg_extension WHERE extname IN (
+ 'edb_job_scheduler', 'dbms_scheduler') """)
+ if status and int(res) == 2:
+ # Get the list of databases specified for the edb_job_scheduler
+ status, res = conn.execute_scalar("""
+ SHOW edb_job_scheduler.database_list""")
+ # If database is available in the specified list than return True.
+ if status and res and conn.db in res:
+ return True
+
+ return False
+
+ @property
+ def module_use_template_javascript(self):
+ """
+ Returns whether Jinja2 template is used for generating the javascript
+ module.
+ """
+ return False
+
+ def register(self, app, options):
+ """
+ Override the default register function to automagically register
+ sub-modules at once.
+ """
+ from .dbms_jobs import blueprint as module
+ self.submodules.append(module)
+
+ from .dbms_programs import blueprint as module
+ self.submodules.append(module)
+
+ from .dbms_schedules import blueprint as module
+ self.submodules.append(module)
+
+ super().register(app, options)
+
+
+blueprint = DBMSJobSchedulerModule(__name__)
+
+
+class DBMSJobSchedulerView(PGChildNodeView):
+ """
+ class DBMSJobSchedulerView(PGChildNodeView)
+
+ A view class for cast node derived from PGChildNodeView. This class is
+ responsible for all the stuff related to view like
+ create/update/delete cast, showing properties of cast node,
+ showing sql in sql pane.
+
+ Methods:
+ -------
+ * __init__(**kwargs)
+ - Method is used to initialize the DBMSJobSchedulerView, 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 attach
+ manager,conn & template_path properties to self
+
+ * nodes()
+ - This function will use to create all the child node within that
+ collection. Here it will create all the scheduler nodes.
+
+ * properties(gid, sid, did, jsid)
+ - This function will show the properties of the selected job node
+
+ """
+
+ node_type = blueprint.node_type
+ BASE_TEMPLATE_PATH = 'dbms_job_scheduler/ppas/#{0}#'
+
+ parent_ids = [
+ {'type': 'int', 'id': 'gid'},
+ {'type': 'int', 'id': 'sid'},
+ {'type': 'int', 'id': 'did'}
+ ]
+ ids = [
+ {'type': 'int', 'id': 'jsid'}
+ ]
+
+ operations = dict({
+ 'obj': [
+ {'get': 'properties'},
+ {'get': 'list'}
+ ],
+ 'children': [{
+ 'get': 'children'
+ }],
+ 'nodes': [{'get': 'nodes'}, {'get': 'nodes'}]
+ })
+
+ def _init_(self, **kwargs):
+ self.conn = None
+ self.template_path = None
+ self.manager = None
+
+ super().__init__(**kwargs)
+
+ def check_precondition(f):
+ """
+ This function will behave as a decorator which will check the
+ database connection before running view. It will also attach
+ manager, conn & template_path properties to self
+ """
+
+ @wraps(f)
+ def wrap(*args, **kwargs):
+ 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'])
+ # Set the template path for the SQL scripts
+ self.template_path = self.BASE_TEMPLATE_PATH.format(
+ self.manager.version)
+
+ # Here args[0] will hold self & kwargs will hold gid,sid,did
+ return f(*args, **kwargs)
+ return wrap
+
+ @check_precondition
+ def nodes(self, gid, sid, did):
+ """
+ This function will use to create all the child nodes within the
+ collection.
+ """
+ return make_json_response(
+ data=[],
+ status=200
+ )
+
+ @check_precondition
+ def list(self, gid, sid, did):
+ """
+ This function will show the run details of all the jobs.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ did: Database ID
+ """
+ try:
+ sql = render_template(
+ "/".join([self.template_path, 'get_job_run_details.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
+ )
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def properties(self, gid, sid, did, jsid):
+ """
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ did: Database ID
+ jsid: Job Scheduler ID
+ """
+ return make_json_response(
+ data=[],
+ status=200
+ )
+
+
+DBMSJobSchedulerView.register_node_view(blueprint)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/__init__.py
new file mode 100644
index 000000000..a913d9137
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/__init__.py
@@ -0,0 +1,785 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+""" Implements DBMS Job objects Node."""
+
+import json
+from functools import wraps
+
+from flask import render_template, request, jsonify
+from flask_babel import gettext
+from pgadmin.browser.collection import CollectionNodeModule
+from pgadmin.browser.server_groups.servers import databases
+from pgadmin.browser.utils import PGChildNodeView
+from pgadmin.utils.ajax import make_json_response, gone, \
+ make_response as ajax_response, internal_server_error, success_return
+from pgadmin.utils.driver import get_driver
+from config import PG_DEFAULT_DRIVER
+from pgadmin.utils.constants import DBMS_JOB_SCHEDULER_ID
+from pgadmin.browser.server_groups.servers.databases.schemas.functions.utils \
+ import format_arguments_from_db
+from pgadmin.browser.server_groups.servers.databases.dbms_job_scheduler.utils \
+ import (resolve_calendar_string, create_calendar_string,
+ get_formatted_program_args)
+
+
+class DBMSJobModule(CollectionNodeModule):
+ """
+ class DBMSJobModule(CollectionNodeModule)
+
+ A module class for DBMS Job objects node derived
+ from CollectionNodeModule.
+
+ Methods:
+ -------
+
+ * get_nodes(gid, sid, did)
+ - Method is used to generate the browser collection node.
+
+ * script_load()
+ - Load the module script for DBMS Job objects, when any of
+ the server node is initialized.
+ """
+ _NODE_TYPE = 'dbms_job'
+ _COLLECTION_LABEL = gettext("DBMS Jobs")
+
+ @property
+ def collection_icon(self):
+ """
+ icon to be displayed for the browser collection node
+ """
+ return 'icon-coll-pga_job'
+
+ @property
+ def node_icon(self):
+ """
+ icon to be displayed for the browser nodes
+ """
+ return 'icon-pga_job'
+
+ def get_nodes(self, gid, sid, did, jsid):
+ """
+ Generate the collection node
+ """
+ if self.show_node:
+ 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 server, when any of the database node is
+ initialized.
+ """
+ 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 = DBMSJobModule(__name__)
+
+
+class DBMSJobView(PGChildNodeView):
+ """
+ class DBMSJobView(PGChildNodeView)
+
+ A view class for DBMSJob node derived from PGChildNodeView.
+ This class is responsible for all the stuff related to view like
+ updating job node, showing properties, showing sql in sql pane.
+
+ Methods:
+ -------
+ * __init__(**kwargs)
+ - Method is used to initialize the DBMSJobView, 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 job nodes within that
+ collection.
+
+ * nodes()
+ - This function will use to create all the child node within that
+ collection. Here it will create all the job node.
+
+ * properties(gid, sid, did, jsid, jsjobid)
+ - This function will show the properties of the selected job node
+
+ * create(gid, sid, did, jsid, jsjobid)
+ - This function will create the new job object
+
+ * msql(gid, sid, did, jsid, jsjobid)
+ - This function is used to return modified SQL for the
+ selected job node
+
+ * sql(gid, sid, did, jsid, jsjobid)
+ - Dummy response for sql panel
+
+ * delete(gid, sid, did, jsid, jsjobid)
+ - Drops job
+ """
+
+ node_type = blueprint.node_type
+ BASE_TEMPLATE_PATH = 'dbms_jobs/ppas/#{0}#'
+ PROGRAM_TEMPLATE_PATH = 'dbms_programs/ppas/#{0}#'
+ SCHEDULE_TEMPLATE_PATH = 'dbms_schedules/ppas/#{0}#'
+
+ parent_ids = [
+ {'type': 'int', 'id': 'gid'},
+ {'type': 'int', 'id': 'sid'},
+ {'type': 'int', 'id': 'did'},
+ {'type': 'int', 'id': 'jsid'}
+ ]
+ ids = [
+ {'type': 'int', 'id': 'jsjobid'}
+ ]
+
+ operations = dict({
+ 'obj': [
+ {'get': 'properties', 'delete': 'delete', 'put': 'update'},
+ {'get': 'list', 'post': 'create', 'delete': 'delete'}
+ ],
+ 'nodes': [{'get': 'nodes'}, {'get': 'nodes'}],
+ 'msql': [{'get': 'msql'}, {'get': 'msql'}],
+ 'sql': [{'get': 'sql'}],
+ 'get_procedures': [{}, {'get': 'get_procedures'}],
+ 'enable_disable': [{'put': 'enable_disable'}],
+ 'get_programs': [{}, {'get': 'get_programs'}],
+ 'get_schedules': [{}, {'get': 'get_schedules'}],
+ 'run_job': [{'put': 'run_job'}],
+ })
+
+ def _init_(self, **kwargs):
+ self.conn = None
+ self.template_path = None
+ self.pr_template_path = None
+ self.sch_template_path = None
+ self.manager = None
+
+ super().__init__(**kwargs)
+
+ def check_precondition(f):
+ """
+ This function will behave as a decorator which will check the
+ database connection before running view. It will also attach
+ 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'])
+ # Set the template path for the SQL scripts
+ self.template_path = self.BASE_TEMPLATE_PATH.format(
+ self.manager.version)
+ self.pr_template_path = self.PROGRAM_TEMPLATE_PATH.format(
+ self.manager.version)
+ self.sch_template_path = self.SCHEDULE_TEMPLATE_PATH.format(
+ self.manager.version)
+
+ return f(*args, **kwargs)
+
+ return wrap
+
+ @check_precondition
+ def list(self, gid, sid, did, jsid):
+ """
+ This function is used to list all the job nodes within
+ that collection.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler 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, jsid, jsjobid=None):
+ """
+ This function is used to create all the child nodes within
+ the collection. Here it will create all the job nodes.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler ID
+ """
+ res = []
+ try:
+ sql = render_template(
+ "/".join([self.template_path, self._NODES_SQL]))
+
+ status, result = self.conn.execute_2darray(sql)
+ if not status:
+ return internal_server_error(errormsg=result)
+
+ if jsjobid is not None:
+ if len(result['rows']) == 0:
+ return gone(
+ errormsg=gettext("Could not find the specified job.")
+ )
+
+ row = result['rows'][0]
+ return make_json_response(
+ data=self.blueprint.generate_browser_node(
+ row['jsjobid'],
+ DBMS_JOB_SCHEDULER_ID,
+ row['jsjobname'],
+ is_enabled=row['jsjobenabled'],
+ icon="icon-pga_job" if row['jsjobenabled'] else
+ "icon-pga_job-disabled",
+ description=row['jsjobdesc']
+ )
+ )
+
+ for row in result['rows']:
+ res.append(
+ self.blueprint.generate_browser_node(
+ row['jsjobid'],
+ DBMS_JOB_SCHEDULER_ID,
+ row['jsjobname'],
+ is_enabled=row['jsjobenabled'],
+ icon="icon-pga_job" if row['jsjobenabled'] else
+ "icon-pga_job-disabled",
+ description=row['jsjobdesc']
+ )
+ )
+
+ return make_json_response(
+ data=res,
+ status=200
+ )
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def properties(self, gid, sid, did, jsid, jsjobid):
+ """
+ This function will show the properties of the selected job node.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler ID
+ jsjobid: Job ID
+ """
+ try:
+ status, data = self._fetch_properties(jsjobid)
+ if not status:
+ return data
+
+ return ajax_response(
+ response=data,
+ status=200
+ )
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ def _fetch_properties(self, jsjobid):
+ """
+ This function is used to fetch the properties.
+ Args:
+ jsjobid:
+ """
+ sql = render_template(
+ "/".join([self.template_path, self._PROPERTIES_SQL]),
+ jsjobid=jsjobid
+ )
+ 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(
+ errormsg=gettext("Could not find the specified job.")
+ )
+
+ data = res['rows'][0]
+
+ # If 'jsjobscname' and 'jsjobprname' in data then set the jsjobtype
+ if ('jsjobscname' in data and data['jsjobscname'] is not None and
+ 'jsjobprname' in data and data['jsjobprname'] is not None):
+ data['jsjobtype'] = 'p'
+ else:
+ data['jsjobtype'] = 's'
+
+ # Resolve the repeat interval string
+ if 'jsscrepeatint' in data:
+ (freq, by_date, by_month, by_month_day, by_weekday, by_hour,
+ by_minute) = resolve_calendar_string(
+ data['jsscrepeatint'])
+
+ data['jsscfreq'] = freq
+ data['jsscdate'] = by_date
+ data['jsscmonths'] = by_month
+ data['jsscmonthdays'] = by_month_day
+ data['jsscweekdays'] = by_weekday
+ data['jsschours'] = by_hour
+ data['jsscminutes'] = by_minute
+
+ # Get Program's arguments
+ sql = render_template(
+ "/".join([self.pr_template_path, self._PROPERTIES_SQL]),
+ jsprid=data['program_id']
+ )
+ status, res_program = self.conn.execute_dict(sql)
+ if not status:
+ return internal_server_error(errormsg=res_program)
+
+ # Update the data dictionary.
+ if len(res_program['rows']) > 0:
+ # Get the formatted program args
+ get_formatted_program_args(self.pr_template_path, self.conn,
+ res_program['rows'][0])
+ # Get the job argument value
+ self.get_job_args_value(self.template_path, self.conn,
+ data['jsjobname'],
+ res_program['rows'][0])
+ data['jsprarguments'] = res_program['rows'][0]['jsprarguments'] \
+ if 'jsprarguments' in res_program['rows'][0] else []
+
+ return True, data
+
+ @check_precondition
+ def create(self, gid, sid, did, jsid):
+ """
+ This function will create the job node.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler ID
+ """
+ data = json.loads(request.data)
+ try:
+ # Get the SQL
+ sql, _, _ = self.get_sql(None, data)
+
+ status, res = self.conn.execute_void('BEGIN')
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ status, res = self.conn.execute_scalar(sql)
+ if not status:
+ if self.conn.connected():
+ self.conn.execute_void('END')
+ return internal_server_error(errormsg=res)
+
+ self.conn.execute_void('END')
+
+ # Get the newly created job id
+ sql = render_template(
+ "/".join([self.template_path, 'get_job_id.sql']),
+ job_name=data['jsjobname'], conn=self.conn
+ )
+ status, res = self.conn.execute_dict(sql)
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ if len(res['rows']) == 0:
+ return gone(
+ errormsg=gettext("Job creation failed.")
+ )
+ row = res['rows'][0]
+
+ return jsonify(
+ node=self.blueprint.generate_browser_node(
+ row['jsjobid'],
+ DBMS_JOB_SCHEDULER_ID,
+ row['jsjobname'],
+ is_enabled=row['jsjobenabled'],
+ icon="icon-pga_job" if row['jsjobenabled'] else
+ "icon-pga_job-disabled"
+ )
+ )
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def update(self, gid, sid, did, jsid, jsjobid=None):
+ """
+ This function will update job object
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler ID
+ """
+ data = request.form if request.form else json.loads(
+ request.data
+ )
+
+ try:
+ sql, jobname, jobenabled = self.get_sql(jsjobid, data)
+
+ status, res = self.conn.execute_scalar(sql)
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ return jsonify(
+ node=self.blueprint.generate_browser_node(
+ jsjobid,
+ DBMS_JOB_SCHEDULER_ID,
+ jobname,
+ icon="icon-pga_job" if jobenabled else
+ "icon-pga_job-disabled"
+ )
+ )
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def delete(self, gid, sid, did, jsid, jsjobid=None):
+ """Delete the Job."""
+
+ if jsjobid is None:
+ data = request.form if request.form else json.loads(
+ request.data
+ )
+ else:
+ data = {'ids': [jsjobid]}
+
+ try:
+ for jsjobid in data['ids']:
+ status, data = self._fetch_properties(jsjobid)
+ if not status:
+ return data
+
+ jsjobname = data['jsjobname']
+
+ status, res = self.conn.execute_void(
+ render_template(
+ "/".join([self.template_path, self._DELETE_SQL]),
+ job_name=jsjobname, conn=self.conn
+ )
+ )
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ return make_json_response(success=1)
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def msql(self, gid, sid, did, jsid, jsjobid=None):
+ """
+ This function is used to return modified SQL for the
+ selected job node.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler ID
+ jsjobid: Job ID (optional)
+ """
+ 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 ('jsjobdesc',):
+ data[k] = v
+ else:
+ data[k] = json.loads(v)
+ except ValueError:
+ data[k] = v
+
+ sql, _, _ = self.get_sql(jsjobid, data)
+
+ return make_json_response(
+ data=sql,
+ status=200
+ )
+
+ def get_sql(self, jsjobid, data):
+ """
+ This function is used to get the SQL.
+ """
+ sql = ''
+ name = ''
+ enabled = True
+ if jsjobid is None:
+ name = data['jsjobname']
+ enabled = data['jsjobenabled']
+
+ # Create calendar string for repeat interval
+ repeat_interval = create_calendar_string(
+ data['jsscfreq'], data['jsscdate'], data['jsscmonths'],
+ data['jsscmonthdays'], data['jsscweekdays'], data['jsschours'],
+ data['jsscminutes'])
+
+ sql = render_template(
+ "/".join([self.template_path, self._CREATE_SQL]),
+ job_name=data['jsjobname'],
+ internal_job_type=data['jsjobtype'],
+ job_type=data['jsprtype'],
+ job_action=data['jsprproc']
+ if data['jsprtype'] == 'STORED_PROCEDURE' else
+ data['jsprcode'],
+ enabled=data['jsjobenabled'],
+ comments=data['jsjobdesc'],
+ number_of_arguments=data['jsprnoofargs'],
+ start_date=data['jsscstart'],
+ repeat_interval=repeat_interval,
+ end_date=data['jsscend'],
+ program_name=data['jsjobprname'],
+ schedule_name=data['jsjobscname'],
+ arguments=data['jsprarguments'] if 'jsprarguments' in data
+ else [],
+ conn=self.conn
+ )
+ elif jsjobid is not None and 'jsprarguments' in data:
+ status, res = self._fetch_properties(jsjobid)
+ if not status:
+ return res
+
+ name = res['jsjobname']
+ enabled = res['jsjobenabled']
+ sql = render_template(
+ "/".join([self.template_path, self._UPDATE_SQL]),
+ job_name=res['jsjobname'],
+ changed_value=data['jsprarguments']['changed'],
+ conn=self.conn
+ )
+
+ return sql, name, enabled
+
+ @check_precondition
+ def sql(self, gid, sid, did, jsid, jsjobid):
+ """
+ This function will generate sql for the sql panel
+ """
+ try:
+ status, data = self._fetch_properties(jsjobid)
+ if not status:
+ return ''
+
+ SQL = render_template(
+ "/".join([self.template_path, self._CREATE_SQL]),
+ display_comments=True,
+ job_name=data['jsjobname'],
+ internal_job_type=data['jsjobtype'],
+ job_type=data['jsprtype'],
+ job_action=data['jsprproc']
+ if data['jsprtype'] == 'STORED_PROCEDURE' else
+ data['jsprcode'],
+ enabled=data['jsjobenabled'],
+ comments=data['jsjobdesc'],
+ number_of_arguments=data['jsprnoofargs'],
+ start_date=data['jsscstart'],
+ repeat_interval=data['jsscrepeatint'],
+ end_date=data['jsscend'],
+ program_name=data['jsjobprname'],
+ schedule_name=data['jsjobscname'],
+ arguments=data['jsprarguments'] if 'jsprarguments' in data
+ else [],
+ conn=self.conn
+ )
+
+ return ajax_response(response=SQL)
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def get_procedures(self, gid, sid, did, jsid=None):
+ """
+ This function will return procedure list
+ :param gid: group id
+ :param sid: server id
+ :param did: database id
+ :return:
+ """
+ res = []
+ sql = render_template("/".join([self.pr_template_path,
+ 'get_procedures.sql']),
+ datlastsysoid=self._DATABASE_LAST_SYSTEM_OID)
+ status, rset = self.conn.execute_dict(sql)
+ if not status:
+ return internal_server_error(errormsg=rset)
+
+ for row in rset['rows']:
+ # Get formatted Arguments
+ frmtd_params, _ = format_arguments_from_db(
+ self.template_path, self.conn, row)
+
+ res.append({'label': row['proc_name'],
+ 'value': row['proc_name'],
+ 'no_of_args': row['number_of_arguments'],
+ 'arguments': frmtd_params['arguments']
+ })
+
+ return make_json_response(
+ data=res,
+ status=200
+ )
+
+ @check_precondition
+ def enable_disable(self, gid, sid, did, jsid, jsjobid=None):
+ """
+ This function is used to enable/disable job.
+ """
+ data = request.form if request.form else json.loads(
+ request.data
+ )
+
+ status, res = self.conn.execute_void(
+ render_template(
+ "/".join([self.pr_template_path, 'enable_disable.sql']),
+ name=data['job_name'],
+ is_enable=data['is_enable_job'], conn=self.conn
+ )
+ )
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ return make_json_response(
+ success=1,
+ info=gettext("Job enabled") if data['is_enable_job'] else
+ gettext('Job disabled'),
+ data={
+ 'sid': sid,
+ 'did': did,
+ 'jsid': jsid,
+ 'jsjobid': jsjobid
+ }
+ )
+
+ @check_precondition
+ def get_programs(self, gid, sid, did, jsid=None):
+ """
+ This function will return procedure list
+ :param gid: group id
+ :param sid: server id
+ :param did: database id
+ :return:
+ """
+ res = []
+ sql = render_template("/".join([self.pr_template_path,
+ self._NODES_SQL]),
+ datlastsysoid=self._DATABASE_LAST_SYSTEM_OID)
+ status, rset = self.conn.execute_dict(sql)
+ if not status:
+ return internal_server_error(errormsg=rset)
+
+ for row in rset['rows']:
+ res.append({'label': row['jsprname'],
+ 'value': row['jsprname']})
+
+ return make_json_response(
+ data=res,
+ status=200
+ )
+
+ @check_precondition
+ def get_schedules(self, gid, sid, did, jsid=None):
+ """
+ This function will return procedure list
+ :param gid: group id
+ :param sid: server id
+ :param did: database id
+ :return:
+ """
+ res = []
+ sql = render_template("/".join([self.sch_template_path,
+ self._NODES_SQL]))
+ status, rset = self.conn.execute_dict(sql)
+ if not status:
+ return internal_server_error(errormsg=rset)
+
+ for row in rset['rows']:
+ res.append({'label': row['jsscname'],
+ 'value': row['jsscname']
+ })
+
+ return make_json_response(
+ data=res,
+ status=200
+ )
+
+ @check_precondition
+ def run_job(self, gid, sid, did, jsid, jsjobid=None):
+ """
+ This function is used to run the job now.
+ """
+ data = request.form if request.form else json.loads(
+ request.data
+ )
+
+ try:
+ status, res = self.conn.execute_void(
+ render_template(
+ "/".join([self.template_path, 'run_job.sql']),
+ job_name=data['job_name'], conn=self.conn
+ )
+ )
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ return success_return(
+ message=gettext("Started the Job execution.")
+ )
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ def get_job_args_value(self, template_path, conn, jobname, data):
+ """
+ This function is used to get the job arguments value.
+ Args:
+ template_path:
+ conn:
+ jobname:
+ data:
+
+ Returns:
+
+ """
+ if 'jsprarguments' in data and len(data['jsprarguments']) > 0:
+ for args in data['jsprarguments']:
+ sql = render_template(
+ "/".join([template_path, 'get_job_args_value.sql']),
+ job_name=jobname,
+ arg_name=args['argname'], conn=self.conn)
+ status, res = conn.execute_scalar(sql)
+ if not status:
+ return internal_server_error(errormsg=res)
+ args['argval'] = res
+
+
+DBMSJobView.register_node_view(blueprint)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/static/js/dbms_job.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/static/js/dbms_job.js
new file mode 100644
index 000000000..8cd44f380
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/static/js/dbms_job.js
@@ -0,0 +1,234 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2024, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import DBMSJobSchema from './dbms_job.ui';
+import { getNodeAjaxOptions } from '../../../../../../../static/js/node_ajax';
+import getApiInstance from '../../../../../../../../static/js/api_instance';
+
+
+define('pgadmin.node.dbms_job', [
+ 'sources/gettext', 'sources/url_for', 'sources/pgadmin',
+ 'pgadmin.browser', 'pgadmin.browser.collection',
+], function(gettext, url_for, pgAdmin, pgBrowser) {
+
+ if (!pgBrowser.Nodes['coll-dbms_job']) {
+ pgBrowser.Nodes['coll-dbms_job'] =
+ pgBrowser.Collection.extend({
+ node: 'dbms_job',
+ label: gettext('DBMS Jobs'),
+ type: 'coll-dbms_job',
+ columns: ['jsjobname', 'jsjobenabled', 'jsjobruncount', 'jsjobfailurecount', 'jsjobdesc'],
+ hasSQL: false,
+ hasDepends: false,
+ hasStatistics: false,
+ hasScriptTypes: [],
+ canDrop: true,
+ canDropCascade: false,
+ });
+ }
+
+ if (!pgBrowser.Nodes['dbms_job']) {
+ pgAdmin.Browser.Nodes['dbms_job'] = pgAdmin.Browser.Node.extend({
+ parent_type: 'dbms_job_scheduler',
+ type: 'dbms_job',
+ label: gettext('DBMS Job'),
+ node_image: 'icon-pga_job',
+ epasHelp: true,
+ epasURL: 'https://www.enterprisedb.com/docs/epas/$VERSION$/epas_compat_bip_guide/03_built-in_packages/15_dbms_scheduler/02_create_job/',
+ dialogHelp: url_for('help.static', {'filename': 'dbms_job.html'}),
+ canDrop: true,
+ hasSQL: true,
+ hasDepends: false,
+ hasStatistics: false,
+ Init: function() {
+ /* Avoid multiple registration of menus */
+ if (this.initialized)
+ return;
+
+ this.initialized = true;
+
+ pgBrowser.add_menus([{
+ name: 'create_dbms_job_on_coll', node: 'coll-dbms_job', module: this,
+ applies: ['object', 'context'], callback: 'show_obj_properties',
+ category: 'create', priority: 4, label: gettext('DBMS Job...'),
+ data: {action: 'create'},
+ },{
+ name: 'create_dbms_job', node: 'dbms_job', module: this,
+ applies: ['object', 'context'], callback: 'show_obj_properties',
+ category: 'create', priority: 4, label: gettext('DBMS Job...'),
+ data: {action: 'create'},
+ },{
+ name: 'create_dbms_job', node: 'dbms_job_scheduler', module: this,
+ applies: ['object', 'context'], callback: 'show_obj_properties',
+ category: 'create', priority: 4, label: gettext('DBMS Job...'),
+ data: {action: 'create'},
+ }, {
+ name: 'enable_job', node: 'dbms_job', module: this,
+ applies: ['object', 'context'], callback: 'enable_job',
+ priority: 4, label: gettext('Enable Job'),
+ enable : 'is_enabled',data: {
+ data_disabled: gettext('Job is already enabled.'),
+ },
+ }, {
+ name: 'disable_job', node: 'dbms_job', module: this,
+ applies: ['object', 'context'], callback: 'disable_job',
+ priority: 4, label: gettext('Disable Job'),
+ enable : 'is_disabled',data: {
+ data_disabled: gettext('Job is already disabled.'),
+ },
+ }, {
+ name: 'run_job', node: 'dbms_job', module: this,
+ applies: ['object', 'context'], callback: 'run_job',
+ priority: 4, label: gettext('Run Job'),
+ enable : 'is_disabled', data: {
+ data_disabled: gettext('Job is already disabled.'),
+ }
+ }
+ ]);
+ },
+ is_enabled: function(node) {
+ return !node?.is_enabled;
+ },
+ is_disabled: function(node) {
+ return node?.is_enabled;
+ },
+ callbacks: {
+ enable_job: function(args, notify) {
+ let input = args || {},
+ obj = this,
+ t = pgBrowser.tree,
+ i = 'item' in input ? input.item : t.selected(),
+ d = i ? t.itemData(i) : undefined;
+
+ if (d) {
+ notify = notify || _.isUndefined(notify) || _.isNull(notify);
+ let enable = function() {
+ let data = d;
+ getApiInstance().put(
+ obj.generate_url(i, 'enable_disable', d, true),
+ {'job_name': data.label, 'is_enable_job': true}
+ ).then(({data: res})=> {
+ if (res.success == 1) {
+ pgAdmin.Browser.notifier.success(res.info);
+ t.removeIcon(i);
+ data.icon = 'icon-pga_jobstep';
+ data.is_enabled = true;
+ t.addIcon(i, {icon: data.icon});
+ t.updateAndReselectNode(i, data);
+ }
+ }).catch(function(error) {
+ pgAdmin.Browser.notifier.pgRespErrorNotify(error);
+ t.refresh(i);
+ });
+ };
+ if (notify) {
+ pgAdmin.Browser.notifier.confirm(
+ gettext('Enable Job'),
+ gettext('Are you sure you want to enable the job %s?', d.label),
+ function() { enable(); },
+ function() { return true;},
+ );
+ } else {
+ enable();
+ }
+ }
+
+ return false;
+ },
+ disable_job: function(args, notify) {
+ let input = args || {},
+ obj = this,
+ t = pgBrowser.tree,
+ i = 'item' in input ? input.item : t.selected(),
+ d = i ? t.itemData(i) : undefined;
+
+ if (d) {
+ notify = notify || _.isUndefined(notify) || _.isNull(notify);
+ let disable = function() {
+ let data = d;
+ getApiInstance().put(
+ obj.generate_url(i, 'enable_disable', d, true),
+ {'job_name': data.label, 'is_enable_job': false}
+ ).then(({data: res})=> {
+ if (res.success == 1) {
+ pgAdmin.Browser.notifier.success(res.info);
+ t.removeIcon(i);
+ data.icon = 'icon-pga_jobstep-disabled';
+ data.is_enabled = false;
+ t.addIcon(i, {icon: data.icon});
+ t.updateAndReselectNode(i, data);
+ }
+ }).catch(function(error) {
+ pgAdmin.Browser.notifier.pgRespErrorNotify(error);
+ t.refresh(i);
+ });
+ };
+ if (notify) {
+ pgAdmin.Browser.notifier.confirm(
+ gettext('Disable Job'),
+ gettext('Are you sure you want to disable the job %s?', d.label),
+ function() { disable(); },
+ function() { return true;},
+ );
+ } else {
+ disable();
+ }
+ }
+ return false;
+ },
+ run_job: function(args, notify) {
+ let input = args || {},
+ obj = this,
+ t = pgBrowser.tree,
+ i = 'item' in input ? input.item : t.selected(),
+ d = i ? t.itemData(i) : undefined;
+
+ if (d) {
+ notify = notify || _.isUndefined(notify) || _.isNull(notify);
+ let run = function() {
+ let data = d;
+ getApiInstance().put(
+ obj.generate_url(i, 'run_job', d, true),
+ {'job_name': data.label}
+ ).then(({data: res})=> {
+ if (res.success == 1) {
+ pgAdmin.Browser.notifier.success(res.info);
+ }
+ }).catch(function(error) {
+ pgAdmin.Browser.notifier.pgRespErrorNotify(error);
+ });
+ };
+ if (notify) {
+ pgAdmin.Browser.notifier.confirm(
+ gettext('Run Job'),
+ gettext('Are you sure you want to run the job %s now?', d.label),
+ function() { run(); },
+ function() { return true;},
+ );
+ } else {
+ run();
+ }
+ }
+ return false;
+ }
+ },
+ getSchema: function(treeNodeInfo, itemNodeData) {
+ return new DBMSJobSchema(
+ {
+ procedures: ()=>getNodeAjaxOptions('get_procedures', this, treeNodeInfo, itemNodeData),
+ programs: ()=>getNodeAjaxOptions('get_programs', this, treeNodeInfo, itemNodeData),
+ schedules: ()=>getNodeAjaxOptions('get_schedules', this, treeNodeInfo, itemNodeData)
+ }
+ );
+ },
+ });
+ }
+
+ return pgBrowser.Nodes['dbms_job'];
+});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/static/js/dbms_job.ui.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/static/js/dbms_job.ui.js
new file mode 100644
index 000000000..e6acd3f51
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/static/js/dbms_job.ui.js
@@ -0,0 +1,198 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2024, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from 'sources/gettext';
+import BaseUISchema from 'sources/SchemaView/base_schema.ui';
+import { isEmptyString } from 'sources/validators';
+import moment from 'moment';
+import { getActionSchema, getRepeatSchema } from '../../../static/js/dbms_job_scheduler_common.ui';
+
+
+export default class DBMSJobSchema extends BaseUISchema {
+ constructor(fieldOptions={}) {
+ super({
+ jsjobid: null,
+ jsjobname: '',
+ jsjobenabled: true,
+ jsjobdesc: '',
+ jsjobtype: 's',
+ jsjobruncount: 0,
+ jsjobfailurecount: 0,
+ // Program Args
+ jsjobprname: '',
+ jsprtype: 'PLSQL_BLOCK',
+ jsprenabled: true,
+ jsprnoofargs: 0,
+ jsprproc: null,
+ jsprcode: null,
+ jsprarguments: [],
+ // Schedule args
+ jsjobscname: '',
+ jsscstart: null,
+ jsscend: null,
+ jsscrepeatint: '',
+ jsscfreq: null,
+ jsscdate: null,
+ jsscweekdays: null,
+ jsscmonthdays: null,
+ jsscmonths: null,
+ jsschours: null,
+ jsscminutes: null,
+ });
+ this.fieldOptions = {
+ procedures: [],
+ programs: [],
+ schedules: [],
+ ...fieldOptions,
+ };
+ }
+
+ get idAttribute() {
+ return 'jsjobid';
+ }
+
+ get baseFields() {
+ let obj = this;
+ return [
+ {
+ id: 'jsjobid', label: gettext('ID'), type: 'int', mode: ['properties'],
+ readonly: function(state) {return !obj.isNew(state); },
+ }, {
+ id: 'jsjobname', label: gettext('Name'), cell: 'text',
+ editable: false, type: 'text', noEmpty: true,
+ readonly: function(state) {return !obj.isNew(state); },
+ }, {
+ id: 'jsjobenabled', label: gettext('Enabled?'), type: 'switch', cell: 'switch',
+ readonly: function(state) {return !obj.isNew(state); },
+ }, {
+ id: 'jsjobtype', label: gettext('Job Type'),
+ type: ()=>{
+ let options = [
+ {'label': gettext('SELF-CONTAINED'), 'value': 's'},
+ {'label': gettext('PRE-DEFINED'), 'value': 'p'},
+ ];
+ return {
+ type: 'toggle',
+ options: options,
+ };
+ },
+ readonly: function(state) {return !obj.isNew(state); },
+ helpMessage: gettext('If the Job Type is Self-Contained you need to specify the action and repeat interval in the Action and Repeat tabs respectively. If the Job Type is Pre-Defined you need to specify the existing Program and Schedule names in the Pre-Defined tab.'),
+ helpMessageMode: ['create'],
+ }, {
+ id: 'jsjobruncount', label: gettext('Run Count'), type: 'int',
+ readonly: true, mode: ['edit', 'properties']
+ }, {
+ id: 'jsjobfailurecount', label: gettext('Failure Count'), type: 'int',
+ readonly: true, mode: ['edit', 'properties']
+ }, {
+ id: 'jsjobdesc', label: gettext('Comment'), type: 'multiline',
+ readonly: function(state) {return !obj.isNew(state); },
+ },
+ // Add the Action Schema
+ ...getActionSchema(obj, 'job'),
+ // Add the Repeat Schema.
+ ...getRepeatSchema(obj, 'job'),
+ {
+ id: 'jsjobprname', label: gettext('Program Name'), type: 'select',
+ controlProps: { allowClear: false}, group: gettext('Pre-Defined'),
+ options: this.fieldOptions.programs,
+ readonly: function(state) {
+ return !obj.isNew(state) || state.jsjobtype == 's';
+ },
+ deps: ['jsjobtype'],
+ depChange: (state) => {
+ if (state.jsjobtype == 's') {
+ return { jsjobprname: null };
+ }
+ }
+ }, {
+ id: 'jsjobscname', label: gettext('Schedule Name'), type: 'select',
+ controlProps: { allowClear: false}, group: gettext('Pre-Defined'),
+ options: this.fieldOptions.schedules,
+ readonly: function(state) {
+ return !obj.isNew(state) || state.jsjobtype == 's';
+ },
+ deps: ['jsjobtype'],
+ depChange: (state) => {
+ if (state.jsjobtype == 's') {
+ return { jsjobscname: null };
+ }
+ }
+ },
+ ];
+ }
+
+ validate(state, setError) {
+ if (state.jsjobtype == 's' ) {
+ if (isEmptyString(state.jsprtype)) {
+ setError('jsprtype', gettext('Job Type cannot be empty.'));
+ return true;
+ } else {
+ setError('jsprtype', null);
+ }
+
+ if (state.jsprtype == 'PLSQL_BLOCK' && isEmptyString(state.jsprcode)) {
+ setError('jsprcode', gettext('Code cannot be empty.'));
+ return true;
+ } else {
+ setError('jsprcode', null);
+ }
+
+ if (state.jsprtype == 'STORED_PROCEDURE' && isEmptyString(state.jsprproc)) {
+ setError('jsprproc', gettext('Procedure cannot be empty.'));
+ return true;
+ } else {
+ setError('jsprproc', null);
+ }
+
+ if (isEmptyString(state.jsscstart) && isEmptyString(state.jsscfreq) &&
+ isEmptyString(state.jsscmonths) && isEmptyString(state.jsscweekdays) &&
+ isEmptyString(state.jsscmonthdays) && isEmptyString(state.jsschours) &&
+ isEmptyString(state.jsscminutes) && isEmptyString(state.jsscdate)) {
+ setError('jsscstart', gettext('Either Start time or Repeat interval must be specified.'));
+ return true;
+ } else {
+ setError('jsscstart', null);
+ }
+
+ if (!isEmptyString(state.jsscend)) {
+ let start_time = state.jsscstart,
+ end_time = state.jsscend,
+ start_time_js = start_time.split(' '),
+ end_time_js = end_time.split(' ');
+
+ start_time_js = moment(start_time_js[0] + ' ' + start_time_js[1]);
+ end_time_js = moment(end_time_js[0] + ' ' + end_time_js[1]);
+
+ if(end_time_js.isBefore(start_time_js)) {
+ setError('jsscend', gettext('Start time must be less than end time'));
+ return true;
+ } else {
+ setError('jsscend', null);
+ }
+ } else {
+ state.jsscend = null;
+ }
+ } else if (state.jsjobtype == 'p') {
+ if (isEmptyString(state.jsjobprname)) {
+ setError('jsjobprname', gettext('Pre-Defined program name cannot be empty.'));
+ return true;
+ } else {
+ setError('jsjobprname', null);
+ }
+ if (isEmptyString(state.jsjobscname)) {
+ setError('jsjobscname', gettext('Pre-Defined schedule name cannot be empty.'));
+ return true;
+ } else {
+ setError('jsjobscname', null);
+ }
+ }
+ }
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/create.sql
new file mode 100644
index 000000000..aac0f82b4
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/create.sql
@@ -0,0 +1,61 @@
+{% if display_comments %}
+-- DBMS Job: '{{ job_name }}'
+
+-- EXEC dbms_scheduler.DROP_JOB('{{ job_name }}');
+
+{% endif %}
+{% if internal_job_type is defined and internal_job_type == 's' %}
+EXEC dbms_scheduler.CREATE_JOB(
+ job_name => {{ job_name|qtLiteral(conn) }},
+ job_type => {{ job_type|qtLiteral(conn) }},
+ job_action => {{ job_action|qtLiteral(conn) }},
+ repeat_interval => {{ repeat_interval|qtLiteral(conn) }}{% if start_date or end_date or number_of_arguments or enabled or comments %},{% endif %}
+{% if start_date %}
+
+ start_date => {{ start_date|qtLiteral(conn) }}{% if end_date or number_of_arguments or enabled or comments %},{% endif %}
+{% endif %}
+{% if end_date %}
+
+ end_date => {{ end_date|qtLiteral(conn) }}{% if number_of_arguments or enabled or comments %},{% endif %}
+{% endif %}
+{% if number_of_arguments %}
+
+ number_of_arguments => {{ number_of_arguments }}{% if enabled or comments %},{% endif %}
+{% endif %}
+{% if enabled %}
+
+ enabled => {{ enabled }}{% if comments %},{% endif %}
+{% endif %}
+{% if comments %}
+
+ comments => {{ comments|qtLiteral(conn) }}
+{% endif %}
+);
+{% elif internal_job_type is defined and internal_job_type == 'p' %}
+EXEC dbms_scheduler.CREATE_JOB(
+ job_name => {{ job_name|qtLiteral(conn) }},
+ program_name => {{ program_name|qtLiteral(conn) }},
+ schedule_name => {{ schedule_name|qtLiteral(conn) }}{% if enabled or comments %},{% endif %}
+{% if enabled %}
+
+ enabled => {{ enabled }}{% if comments %},{% endif %}
+{% endif %}
+{% if comments %}
+
+ comments => {{ comments|qtLiteral(conn) }}
+{% endif %}
+);
+{% endif %}
+
+{% for args_list_item in arguments %}
+EXEC dbms_scheduler.SET_JOB_ARGUMENT_VALUE(
+ job_name => {{ job_name|qtLiteral(conn) }},
+ argument_name => {{ args_list_item.argname|qtLiteral(conn) }},
+{% if args_list_item.argval is defined and args_list_item.argval != '' %}
+ argument_value => {{ args_list_item.argval|qtLiteral(conn) }}
+{% elif args_list_item.argdefval is defined %}
+ argument_value => {{ args_list_item.argdefval|qtLiteral(conn) }}
+{% endif %}
+);
+
+{% endfor %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/delete.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/delete.sql
new file mode 100644
index 000000000..00342ead0
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/delete.sql
@@ -0,0 +1,3 @@
+EXEC dbms_scheduler.DROP_JOB(
+ {{ job_name|qtLiteral(conn) }}
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/get_job_args_value.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/get_job_args_value.sql
new file mode 100644
index 000000000..2b187515e
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/get_job_args_value.sql
@@ -0,0 +1,3 @@
+SELECT value
+FROM dba_scheduler_job_args
+WHERE job_name = {{ job_name|qtLiteral(conn) }} AND argument_name = {{ arg_name|qtLiteral(conn) }}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/get_job_id.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/get_job_id.sql
new file mode 100644
index 000000000..240cfec8f
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/get_job_id.sql
@@ -0,0 +1,5 @@
+SELECT
+ dsj_job_id AS jsjobid, dsj_job_name AS jsjobname,
+ dsj_enabled AS jsjobenabled
+FROM sys.scheduler_0400_job
+WHERE dsj_job_name={{ job_name|qtLiteral(conn) }}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/nodes.sql
new file mode 100644
index 000000000..7e5a5fcce
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/nodes.sql
@@ -0,0 +1,4 @@
+SELECT
+ dsj_job_id as jsjobid, dsj_job_name as jsjobname,
+ dsj_enabled as jsjobenabled, dsj_comments as jsjobdesc
+FROM sys.scheduler_0400_job;
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/properties.sql
new file mode 100644
index 000000000..baa85c808
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/properties.sql
@@ -0,0 +1,14 @@
+SELECT
+ job.dsj_job_id as jsjobid, job_name as jsjobname, program_name as jsjobprname, job_type as jsprtype,
+ CASE WHEN job_type = 'PLSQL_BLOCK' THEN job_action ELSE '' END AS jsprcode,
+ CASE WHEN job_type = 'STORED_PROCEDURE' THEN job_action ELSE '' END AS jsprproc,
+ job_action, number_of_arguments as jsprnoofargs, schedule_name as jsjobscname,
+ start_date as jsscstart, end_date as jsscend, repeat_interval as jsscrepeatint,
+ enabled as jsjobenabled, comments as jsjobdesc,
+ run_count as jsjobruncount, failure_count as jsjobfailurecount,
+ job.dsj_program_id as program_id, job.dsj_schedule_id as schedule_id
+FROM sys.dba_scheduler_jobs jobv
+ LEFT JOIN sys.scheduler_0400_job job ON jobv.job_name = job.dsj_job_name
+{% if jsjobid %}
+WHERE job.dsj_job_id={{jsjobid}}::oid
+{% endif %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/run_job.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/run_job.sql
new file mode 100644
index 000000000..ad5e857e3
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/run_job.sql
@@ -0,0 +1,3 @@
+EXEC dbms_scheduler.RUN_JOB(
+ {{ job_name|qtLiteral(conn) }}
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/update.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/update.sql
new file mode 100644
index 000000000..eeb886e50
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/update.sql
@@ -0,0 +1,12 @@
+{% for chval in changed_value %}
+EXEC dbms_scheduler.SET_JOB_ARGUMENT_VALUE(
+ job_name => {{ job_name|qtLiteral(conn) }},
+ argument_name => {{ chval.argname|qtLiteral(conn) }},
+{% if chval.argval is defined and chval.argval != '' %}
+ argument_value => {{ chval.argval|qtLiteral(conn) }}
+{% elif chval.argdefval is defined %}
+ argument_value => {{ chval.argdefval|qtLiteral(conn) }}
+{% endif %}
+);
+
+{% endfor %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/dbms_jobs_test_data.json b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/dbms_jobs_test_data.json
new file mode 100644
index 000000000..5216e2bf1
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/dbms_jobs_test_data.json
@@ -0,0 +1,429 @@
+{
+ "dbms_create_job": [
+ {
+ "name": "Create job when type is self contained and PLSQL_BLOCK",
+ "url": "/browser/dbms_job/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsjobid": null,
+ "jsjobname": "job_self_with_psql",
+ "jsjobenabled": true,
+ "jsjobdesc": "This is a self contained psql job.",
+ "jsjobtype": "s",
+ "jsprtype": "PLSQL_BLOCK",
+ "jsprnoofargs": 0,
+ "jsprarguments": [],
+ "jsprproc": null,
+ "jsprcode": "BEGIN PERFORM 1; END;",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2054-02-28 00:00:00 +05:30",
+ "jsscfreq": "YEARLY",
+ "jsscdate": null,
+ "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"],
+ "jsscmonthdays": ["2", "8", "31", "27"],
+ "jsscmonths": ["1", "5", "12"],
+ "jsschours": ["05", "18", "22"],
+ "jsscminutes": ["45", "37", "58"],
+ "jsjobprname": "",
+ "jsjobscname": ""
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create job when type is self contained and STORED_PROCEDURE without arguments",
+ "url": "/browser/dbms_job/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsjobid": null,
+ "jsjobname": "job_self_with_proc_noargs",
+ "jsjobenabled": true,
+ "jsjobdesc": "This is a self contained stored procedure job with no args.",
+ "jsjobtype": "s",
+ "jsprtype": "STORED_PROCEDURE",
+ "jsprnoofargs": 0,
+ "jsprarguments": [],
+ "jsprproc": "public.test_proc_without_args",
+ "jsprcode": null,
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2054-02-28 00:00:00 +05:30",
+ "jsscfreq": "YEARLY",
+ "jsscdate": "20250113",
+ "jsscweekdays": [],
+ "jsscmonthdays": [],
+ "jsscmonths": [],
+ "jsschours": [],
+ "jsscminutes": [],
+ "jsjobprname": "",
+ "jsjobscname": ""
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create job when type is pre-defined and program is PLSQL",
+ "url": "/browser/dbms_job/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsjobid": null,
+ "jsjobname": "job_pre_with_psql",
+ "jsjobenabled": true,
+ "jsjobdesc": "This is a pre-defined job with PLSQL program.",
+ "jsjobtype": "p",
+ "jsjobprname": "prg_with_psql",
+ "jsjobscname": "yearly_sch",
+ "jsprtype": null,
+ "jsprnoofargs": 0,
+ "jsprarguments": [],
+ "jsprproc": null,
+ "jsprcode": null,
+ "jsscstart": null,
+ "jsscend": null,
+ "jsscfreq": null,
+ "jsscdate": null,
+ "jsscweekdays": null,
+ "jsscmonthdays": null,
+ "jsscmonths": null,
+ "jsschours": null,
+ "jsscminutes": null
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create job when type is pre-defined and program is Stored Procedure without args",
+ "url": "/browser/dbms_job/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsjobid": null,
+ "jsjobname": "job_pre_with_proc_noargs",
+ "jsjobenabled": true,
+ "jsjobdesc": "This is a pre-defined job with Stored Procedure without args",
+ "jsjobtype": "p",
+ "jsjobprname": "prg_with_proc_noargs",
+ "jsjobscname": "yearly_sch",
+ "jsprtype": null,
+ "jsprnoofargs": 0,
+ "jsprarguments": [],
+ "jsprproc": null,
+ "jsprcode": null,
+ "jsscstart": null,
+ "jsscend": null,
+ "jsscfreq": null,
+ "jsscdate": null,
+ "jsscweekdays": null,
+ "jsscmonthdays": null,
+ "jsscmonths": null,
+ "jsschours": null,
+ "jsscminutes": null
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create job when type is pre-defined and program is Stored Procedure with args",
+ "url": "/browser/dbms_job/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsjobid": null,
+ "jsjobname": "job_pre_with_proc_args",
+ "jsjobenabled": true,
+ "jsjobdesc": "This is a pre-defined job with program is Stored Procedure with args.",
+ "jsjobtype": "p",
+ "jsjobprname": "prg_with_proc_args",
+ "jsjobscname": "yearly_sch",
+ "jsprtype": null,
+ "jsprnoofargs": [],
+ "jsprarguments": [],
+ "jsprproc": null,
+ "jsprcode": null,
+ "jsscstart": null,
+ "jsscend": null,
+ "jsscfreq": null,
+ "jsscdate": null,
+ "jsscweekdays": null,
+ "jsscmonthdays": null,
+ "jsscmonths": null,
+ "jsschours": null,
+ "jsscminutes": null
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create job: while server is down",
+ "url": "/browser/dbms_job/obj/",
+ "is_positive_test": false,
+ "test_data": {
+ "jsjobid": null,
+ "jsjobname": "job_with_psql",
+ "jsjobenabled": true,
+ "jsjobdesc": "This is a self contained psql job.",
+ "jsjobtype": "s",
+ "jsprtype": "PLSQL_BLOCK",
+ "jsprnoofargs": 0,
+ "jsprarguments": [],
+ "jsprproc": null,
+ "jsprcode": "BEGIN PERFORM 1; END;",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2054-02-28 00:00:00 +05:30",
+ "jsscfreq": "YEARLY",
+ "jsscdate": null,
+ "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"],
+ "jsscmonthdays": ["2", "8", "31", "27"],
+ "jsscmonths": ["1", "5", "12"],
+ "jsschours": ["05", "18", "22"],
+ "jsscminutes": ["45", "37", "58"],
+ "jsjobprname": "",
+ "jsjobscname": ""
+ },
+ "mocking_required": true,
+ "mock_data": {
+ "function_name": "pgadmin.utils.driver.psycopg3.connection.Connection.execute_scalar",
+ "return_value": "[(False,'Mocked Internal Server Error')]"
+ },
+ "expected_data": {
+ "status_code": 500,
+ "error_msg": "Mocked Internal Server Error",
+ "test_result_data": {}
+ }
+ }
+ ],
+ "dbms_update_job": [
+ {
+ "name": "Set job argument value",
+ "url": "/browser/dbms_job/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsjobname": "job_with_update_args",
+ "jsprarguments": {
+ "changed": [{"argid":0,"argtype":"bigint","argmode":"IN","argname":"salary","argdefval":"10000","argval":"5000"}]
+ }
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ }
+ ],
+ "dbms_delete_job": [
+ {
+ "name": "Delete job: With existing DBMS job.",
+ "url": "/browser/dbms_job/obj/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": false
+ },
+ {
+ "name": "Delete multiple jobs: With existing DBMS jobs.",
+ "url": "/browser/dbms_job/obj/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": true
+ }
+ ],
+ "dbms_get_job": [
+ {
+ "name": "Get job: With existing DBMS job.",
+ "url": "/browser/dbms_job/obj/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": false
+ },
+ {
+ "name": "Get jobs: With multiple existing DBMS jobs.",
+ "url": "/browser/dbms_job/obj/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": true
+ },
+ {
+ "name": "Get job: while server down.",
+ "url": "/browser/dbms_job/obj/",
+ "is_positive_test": false,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": true,
+ "mock_data": {
+ "function_name": "pgadmin.utils.driver.psycopg3.connection.Connection.execute_dict",
+ "return_value": "(False,'Mocked Internal Server Error')"
+ },
+ "expected_data": {
+ "status_code": 500,
+ "error_msg": "Mocked Internal Server Error",
+ "test_result_data": {}
+ },
+ "is_list": false
+ }
+ ],
+ "dbms_msql_job": [
+ {
+ "name": "Get job msql: For existing PLSQL job.",
+ "url": "/browser/dbms_job/msql/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {
+ "jsjobid": null,
+ "jsjobname": "job_self_with_psql",
+ "jsjobenabled": true,
+ "jsjobdesc": "This is a self contained psql job.",
+ "jsjobtype": "s",
+ "jsprtype": "PLSQL_BLOCK",
+ "jsprnoofargs": 0,
+ "jsprarguments": [],
+ "jsprproc": null,
+ "jsprcode": "BEGIN PERFORM 1; END;",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2054-02-28 00:00:00 +05:30",
+ "jsscfreq": "YEARLY",
+ "jsscdate": null,
+ "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"],
+ "jsscmonthdays": ["2", "8", "31", "27"],
+ "jsscmonths": ["1", "5", "12"],
+ "jsschours": ["05", "18", "22"],
+ "jsscminutes": ["45", "37", "58"],
+ "jsjobprname": "",
+ "jsjobscname": ""
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": false
+ },
+ {
+ "name": "Get job msql: For existing STORED_PROCEDURE job.",
+ "url": "/browser/dbms_job/msql/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {
+ "jsjobid": null,
+ "jsjobname": "job_self_with_proc_noargs",
+ "jsjobenabled": true,
+ "jsjobdesc": "This is a self contained stored procedure job with no args.",
+ "jsjobtype": "s",
+ "jsprtype": "STORED_PROCEDURE",
+ "jsprnoofargs": 0,
+ "jsprarguments": [],
+ "jsprproc": "public.test_proc_without_args",
+ "jsprcode": null,
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2054-02-28 00:00:00 +05:30",
+ "jsscfreq": "YEARLY",
+ "jsscdate": "20250113",
+ "jsscweekdays": [],
+ "jsscmonthdays": [],
+ "jsscmonths": [],
+ "jsschours": [],
+ "jsscminutes": [],
+ "jsjobprname": "",
+ "jsjobscname": ""
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": false
+ }
+ ],
+ "dbms_enable_job": [
+ {
+ "name": "Enable existing job",
+ "url": "/browser/dbms_job/enable_disable/",
+ "is_positive_test": true,
+ "test_data": {
+ "is_enable_job": true
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ }
+ ],
+ "dbms_disable_job": [
+ {
+ "name": "Disable existing job",
+ "url": "/browser/dbms_job/enable_disable/",
+ "is_positive_test": true,
+ "test_data": {
+ "is_enable_job": false
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ }
+ ]
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_add_job.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_add_job.py
new file mode 100644
index 000000000..4f4bbf2fe
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_add_job.py
@@ -0,0 +1,94 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import os
+import json
+from unittest.mock import patch
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_jobs_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSAddJobTestCase(BaseTestGenerator):
+ """This class will test the add job in the DBMS Job API"""
+ scenarios = utils.generate_scenarios("dbms_create_job",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ # Create job schedule
+ job_scheduler_utils.create_dbms_schedule(self, 'yearly_sch')
+ job_scheduler_utils.create_dbms_program(self, 'prg_with_psql')
+ job_scheduler_utils.create_dbms_program(
+ self,'prg_with_proc_noargs', with_proc=True,
+ proc_name='public.test_proc_without_args()')
+ job_scheduler_utils.create_dbms_program(
+ self,'prg_with_proc_args', with_proc=True,
+ proc_name='public.test_proc_with_args(IN salary bigint DEFAULT '
+ '10000, IN name character varying)')
+
+ def runTest(self):
+ """ This function will add DBMS Job under test database. """
+ if self.is_positive_test:
+ response = job_scheduler_utils.api_create(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ # Verify in backend
+ response_data = json.loads(response.data)
+ self.jobs_id = response_data['node']['_id']
+ jobs_name = response_data['node']['label']
+ is_present = job_scheduler_utils.verify_dbms_job(
+ self, jobs_name)
+ self.assertTrue(
+ is_present,"DBMS job was not created successfully.")
+ else:
+ if self.mocking_required:
+ with patch(self.mock_data["function_name"],
+ side_effect=eval(self.mock_data["return_value"])):
+ response = job_scheduler_utils.api_create(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+ utils.assert_error_message(self, response)
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_delete_job.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_delete_job.py
new file mode 100644
index 000000000..f8e6a5bd1
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_delete_job.py
@@ -0,0 +1,98 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import uuid
+import os
+import json
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_jobs_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSDeleteJobTestCase(BaseTestGenerator):
+ """This class will test the delete job in the DBMS job API"""
+ scenarios = utils.generate_scenarios("dbms_delete_job",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ self.job_name = "test_job_delete%s" % str(uuid.uuid4())[1:8]
+ self.job_id = job_scheduler_utils.create_dbms_job(
+ self, self.job_name)
+
+ # multiple jobs
+ if self.is_list:
+ self.job_name2 = "test_job_delete%s" % str(uuid.uuid4())[1:8]
+ self.job_id_2 = job_scheduler_utils.create_dbms_job(
+ self, self.job_name2)
+
+ def runTest(self):
+ """
+ This function will test delete DBMS job under test database.
+ """
+ if self.is_list:
+ self.data['ids'] = [self.job_id, self.job_id_2]
+ response = job_scheduler_utils.api_delete(self, '')
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ is_present = job_scheduler_utils.verify_dbms_job(
+ self, self.job_name)
+ self.assertFalse(
+ is_present, "DBMS job was not deleted successfully")
+
+ is_present = job_scheduler_utils.verify_dbms_job(
+ self, self.job_name2)
+ self.assertFalse(
+ is_present, "DBMS job was not deleted successfully")
+ else:
+ response = job_scheduler_utils.api_delete(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ is_present = job_scheduler_utils.verify_dbms_job(
+ self, self.job_name)
+ self.assertFalse(
+ is_present, "DBMS job was not deleted successfully")
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_disable_job.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_disable_job.py
new file mode 100644
index 000000000..4f9f47487
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_disable_job.py
@@ -0,0 +1,71 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import uuid
+import os
+import json
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_jobs_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSDisableJobTestCase(BaseTestGenerator):
+ """This class will test the add job in the DBMS job API"""
+ scenarios = utils.generate_scenarios("dbms_disable_job",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ self.job_name = "test_job_disable%s" % str(uuid.uuid4())[1:8]
+ self.data['job_name'] = self.job_name
+ self.job_id = job_scheduler_utils.create_dbms_job(
+ self, self.job_name)
+
+ def runTest(self):
+ """ This function will test DBMS job under test database."""
+ response = job_scheduler_utils.api_put(self, self.job_id)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.delete_dbms_job(self, self.job_name)
+
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_enable_job.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_enable_job.py
new file mode 100644
index 000000000..78b3ef4b4
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_enable_job.py
@@ -0,0 +1,71 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import uuid
+import os
+import json
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_jobs_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSEnableJobTestCase(BaseTestGenerator):
+ """This class will test the enable job in the DBMS job API"""
+ scenarios = utils.generate_scenarios("dbms_enable_job",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ self.job_name = "test_job_enable%s" % str(uuid.uuid4())[1:8]
+ self.data['job_name'] = self.job_name
+ self.job_id = job_scheduler_utils.create_dbms_job(
+ self, self.job_name, False)
+
+ def runTest(self):
+ """ This function will test DBMS job under test database."""
+ response = job_scheduler_utils.api_put(self, self.job_id)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.delete_dbms_job(self, self.job_name)
+
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_get_job.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_get_job.py
new file mode 100644
index 000000000..7f28410de
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_get_job.py
@@ -0,0 +1,92 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import uuid
+import os
+import json
+from unittest.mock import patch
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_jobs_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSGetJobTestCase(BaseTestGenerator):
+ """This class will test the add job in the DBMS job API"""
+ scenarios = utils.generate_scenarios("dbms_get_job",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ self.job_name = "test_job_get%s" % str(uuid.uuid4())[1:8]
+ self.job_id = job_scheduler_utils.create_dbms_job(
+ self, self.job_name)
+
+ # multiple jobs
+ if self.is_list:
+ self.job_name2 = "test_job_get%s" % str(uuid.uuid4())[1:8]
+ self.job_id_2 = job_scheduler_utils.create_dbms_job(
+ self, self.job_name2,)
+
+ def runTest(self):
+ """ This function will test DBMS job under test database."""
+ if self.is_positive_test:
+ if self.is_list:
+ response = job_scheduler_utils.api_get(self, '')
+ else:
+ response = job_scheduler_utils.api_get(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+ else:
+ if self.mocking_required:
+ with patch(self.mock_data["function_name"],
+ side_effect=[eval(self.mock_data["return_value"])]):
+ response = job_scheduler_utils.api_get(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+ utils.assert_error_message(self, response)
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.delete_dbms_job(self, self.job_name)
+ if self.is_list:
+ job_scheduler_utils.delete_dbms_job(self, self.job_name2)
+
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_get_msql_job.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_get_msql_job.py
new file mode 100644
index 000000000..d08ff2c96
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_get_msql_job.py
@@ -0,0 +1,65 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import os
+import json
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_jobs_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSGetMSQLJobTestCase(BaseTestGenerator):
+ """This class will test the add job in the DBMS job API"""
+ scenarios = utils.generate_scenarios("dbms_msql_job",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ def runTest(self):
+ """ This function will add DBMS job under test database. """
+ url_encode_data = self.data
+
+ response = job_scheduler_utils.api_get_msql(self, url_encode_data)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_update_job.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_update_job.py
new file mode 100644
index 000000000..1a6832b9f
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_update_job.py
@@ -0,0 +1,73 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import os
+import json
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_jobs_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSUpdateJobTestCase(BaseTestGenerator):
+ """This class will test the add job in the DBMS Job API"""
+ scenarios = utils.generate_scenarios("dbms_update_job",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ # Create job schedule
+ job_scheduler_utils.create_dbms_schedule(self, 'yearly_sch')
+ job_scheduler_utils.create_dbms_program(
+ self,'prg_with_proc_args', with_proc=True,
+ proc_name='public.test_proc_with_args()',
+ define_args=True)
+ self.job_id = job_scheduler_utils.create_dbms_job(
+ self, self.data['jsjobname'], True,
+ 'prg_with_proc_args','yearly_sch')
+
+ def runTest(self):
+ """ This function will update DBMS Job under test database. """
+ response = job_scheduler_utils.api_put(self, self.job_id)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/__init__.py
new file mode 100644
index 000000000..c37220411
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/__init__.py
@@ -0,0 +1,574 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+""" Implements DBMS Program objects Node."""
+
+import json
+from functools import wraps
+
+from flask import render_template, request, jsonify
+from flask_babel import gettext
+from pgadmin.browser.collection import CollectionNodeModule
+from pgadmin.browser.server_groups.servers import databases
+from pgadmin.browser.utils import PGChildNodeView
+from pgadmin.utils.ajax import make_json_response, gone, \
+ make_response as ajax_response, internal_server_error
+from pgadmin.utils.driver import get_driver
+from config import PG_DEFAULT_DRIVER
+from pgadmin.utils.constants import DBMS_JOB_SCHEDULER_ID
+from pgadmin.browser.server_groups.servers.databases.schemas.functions.utils \
+ import format_arguments_from_db
+from pgadmin.browser.server_groups.servers.databases.dbms_job_scheduler.utils \
+ import get_formatted_program_args
+
+
+class DBMSProgramModule(CollectionNodeModule):
+ """
+ class DBMSProgramModule(CollectionNodeModule)
+
+ A module class for DBMS Program objects node derived
+ from CollectionNodeModule.
+
+ Methods:
+ -------
+
+ * get_nodes(gid, sid, did)
+ - Method is used to generate the browser collection node.
+
+ * script_load()
+ - Load the module script for DBMS Program objects, when any of
+ the server node is initialized.
+ """
+ _NODE_TYPE = 'dbms_program'
+ _COLLECTION_LABEL = gettext("DBMS Programs")
+
+ @property
+ def collection_icon(self):
+ """
+ icon to be displayed for the browser collection node
+ """
+ return 'icon-coll-pga_jobstep'
+
+ @property
+ def node_icon(self):
+ """
+ icon to be displayed for the browser nodes
+ """
+ return 'icon-pga_jobstep'
+
+ def get_nodes(self, gid, sid, did, jsid):
+ """
+ Generate the collection node
+ """
+ if self.show_node:
+ 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 server, when any of the database node is
+ initialized.
+ """
+ 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 = DBMSProgramModule(__name__)
+
+
+class DBMSProgramView(PGChildNodeView):
+ """
+ class DBMSProgramView(PGChildNodeView)
+
+ A view class for DBMSProgram node derived from PGChildNodeView.
+ This class is responsible for all the stuff related to view like
+ updating program node, showing properties, showing sql in sql pane.
+
+ Methods:
+ -------
+ * __init__(**kwargs)
+ - Method is used to initialize the DBMSProgramView, 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 program nodes within that
+ collection.
+
+ * nodes()
+ - This function will use to create all the child node within that
+ collection. Here it will create all the program node.
+
+ * properties(gid, sid, did, jsid, jsprid)
+ - This function will show the properties of the selected program node
+
+ * create(gid, sid, did, jsid, jsprid)
+ - This function will create the new program object
+
+ * msql(gid, sid, did, jsid, jsprid)
+ - This function is used to return modified SQL for the
+ selected program node
+
+ * sql(gid, sid, did, jsid, jsprid)
+ - Dummy response for sql panel
+
+ * delete(gid, sid, did, jsid, jsprid)
+ - Drops job program
+ """
+
+ node_type = blueprint.node_type
+ BASE_TEMPLATE_PATH = 'dbms_programs/ppas/#{0}#'
+
+ parent_ids = [
+ {'type': 'int', 'id': 'gid'},
+ {'type': 'int', 'id': 'sid'},
+ {'type': 'int', 'id': 'did'},
+ {'type': 'int', 'id': 'jsid'}
+ ]
+ ids = [
+ {'type': 'int', 'id': 'jsprid'}
+ ]
+
+ operations = dict({
+ 'obj': [
+ {'get': 'properties', 'delete': 'delete'},
+ {'get': 'list', 'post': 'create', 'delete': 'delete'}
+ ],
+ 'nodes': [{'get': 'nodes'}, {'get': 'nodes'}],
+ 'msql': [{'get': 'msql'}, {'get': 'msql'}],
+ 'sql': [{'get': 'sql'}],
+ 'get_procedures': [{}, {'get': 'get_procedures'}],
+ 'enable_disable': [{'put': 'enable_disable'}],
+ })
+
+ def _init_(self, **kwargs):
+ self.conn = None
+ self.template_path = None
+ self.manager = None
+
+ super().__init__(**kwargs)
+
+ def check_precondition(f):
+ """
+ This function will behave as a decorator which will check the
+ database connection before running view. It will also attach
+ 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'])
+ # Set the template path for the SQL scripts
+ self.template_path = self.BASE_TEMPLATE_PATH.format(
+ self.manager.version)
+
+ return f(*args, **kwargs)
+
+ return wrap
+
+ @check_precondition
+ def list(self, gid, sid, did, jsid):
+ """
+ This function is used to list all the program nodes within
+ that collection.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler 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, jsid, jsprid=None):
+ """
+ This function is used to create all the child nodes within
+ the collection. Here it will create all the program nodes.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler ID
+ """
+ res = []
+ try:
+ sql = render_template(
+ "/".join([self.template_path, self._NODES_SQL]))
+
+ status, result = self.conn.execute_2darray(sql)
+ if not status:
+ return internal_server_error(errormsg=result)
+
+ if jsprid is not None:
+ if len(result['rows']) == 0:
+ return gone(
+ errormsg=gettext("Could not find the specified "
+ "program."))
+
+ row = result['rows'][0]
+ return make_json_response(
+ data=self.blueprint.generate_browser_node(
+ row['jsprid'],
+ DBMS_JOB_SCHEDULER_ID,
+ row['jsprname'],
+ is_enabled=row['jsprenabled'],
+ icon="icon-pga_jobstep" if row['jsprenabled'] else
+ "icon-pga_jobstep-disabled",
+ description=row['jsprdesc']
+ )
+ )
+
+ for row in result['rows']:
+ res.append(
+ self.blueprint.generate_browser_node(
+ row['jsprid'],
+ DBMS_JOB_SCHEDULER_ID,
+ row['jsprname'],
+ is_enabled=row['jsprenabled'],
+ icon="icon-pga_jobstep" if row['jsprenabled'] else
+ "icon-pga_jobstep-disabled",
+ description=row['jsprdesc']
+ )
+ )
+
+ return make_json_response(
+ data=res,
+ status=200
+ )
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def properties(self, gid, sid, did, jsid, jsprid):
+ """
+ This function will show the properties of the selected program node.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler ID
+ jsprid: Job program ID
+ """
+ try:
+ sql = render_template(
+ "/".join([self.template_path, self._PROPERTIES_SQL]),
+ jsprid=jsprid
+ )
+ status, res = self.conn.execute_dict(sql)
+
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ if len(res['rows']) == 0:
+ return gone(
+ errormsg=gettext("Could not find the specified program.")
+ )
+
+ data = res['rows'][0]
+ # Get the formatted program args
+ get_formatted_program_args(self.template_path, self.conn, data)
+
+ return ajax_response(
+ response=data,
+ status=200
+ )
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def create(self, gid, sid, did, jsid):
+ """
+ This function will update the data for the selected program node.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler ID
+ """
+ data = json.loads(request.data)
+ try:
+ sql = render_template(
+ "/".join([self.template_path, self._CREATE_SQL]),
+ program_name=data['jsprname'],
+ program_type=data['jsprtype'],
+ program_action=data['jsprproc']
+ if data['jsprtype'] == 'STORED_PROCEDURE' else
+ data['jsprcode'],
+ number_of_arguments=data['jsprnoofargs'],
+ enabled=data['jsprenabled'],
+ comments=data['jsprdesc'],
+ arguments=data['jsprarguments'],
+ conn=self.conn
+ )
+
+ status, res = self.conn.execute_void('BEGIN')
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ status, res = self.conn.execute_scalar(sql)
+ if not status:
+ if self.conn.connected():
+ self.conn.execute_void('END')
+ return internal_server_error(errormsg=res)
+
+ self.conn.execute_void('END')
+
+ # Get the newly created program id
+ sql = render_template(
+ "/".join([self.template_path, 'get_program_id.sql']),
+ jsprname=data['jsprname'], conn=self.conn
+ )
+ status, res = self.conn.execute_dict(sql)
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ if len(res['rows']) == 0:
+ return gone(
+ errormsg=gettext("Job program creation failed.")
+ )
+ row = res['rows'][0]
+
+ return jsonify(
+ node=self.blueprint.generate_browser_node(
+ row['jsprid'],
+ DBMS_JOB_SCHEDULER_ID,
+ row['jsprname'],
+ is_enabled=row['jsprenabled'],
+ icon="icon-pga_jobstep" if row['jsprenabled'] else
+ "icon-pga_jobstep-disabled"
+ )
+ )
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def delete(self, gid, sid, did, jsid, jsprid=None):
+ """Delete the Job program."""
+
+ if jsprid is None:
+ data = request.form if request.form else json.loads(
+ request.data
+ )
+ else:
+ data = {'ids': [jsprid]}
+
+ try:
+ for jsprid in data['ids']:
+ sql = render_template(
+ "/".join([self.template_path, self._PROPERTIES_SQL]),
+ jsprid=jsprid
+ )
+
+ status, res = self.conn.execute_dict(sql)
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ jsprname = res['rows'][0]['jsprname']
+
+ status, res = self.conn.execute_void(
+ render_template(
+ "/".join([self.template_path, self._DELETE_SQL]),
+ program_name=jsprname, conn=self.conn
+ )
+ )
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ return make_json_response(success=1)
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def msql(self, gid, sid, did, jsid, jsprid=None):
+ """
+ This function is used to return modified SQL for the
+ selected program node.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler ID
+ jsprid: Job program ID (optional)
+ """
+ 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 ('jsprdesc',):
+ data[k] = v
+ else:
+ data[k] = json.loads(v)
+ except ValueError:
+ data[k] = v
+
+ try:
+ sql = render_template(
+ "/".join([self.template_path, self._CREATE_SQL]),
+ program_name=data['jsprname'],
+ program_type=data['jsprtype'],
+ program_action=data['jsprproc']
+ if data['jsprtype'] == 'STORED_PROCEDURE' else
+ data['jsprcode'],
+ number_of_arguments=data['jsprnoofargs'],
+ enabled=data['jsprenabled'],
+ comments=data['jsprdesc'],
+ arguments=data['jsprarguments'],
+ conn=self.conn
+ )
+
+ return make_json_response(
+ data=sql,
+ status=200
+ )
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def sql(self, gid, sid, did, jsid, jsprid):
+ """
+ This function will generate sql for the sql panel
+ """
+ try:
+ SQL = render_template("/".join(
+ [self.template_path, self._PROPERTIES_SQL]
+ ), jsprid=jsprid)
+
+ status, res = self.conn.execute_dict(SQL)
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ if len(res['rows']) == 0:
+ return gone(
+ gettext("Could not find the DBMS Schedule.")
+ )
+
+ data = res['rows'][0]
+ # Get the formatted program args
+ get_formatted_program_args(self.template_path, self.conn, data)
+
+ SQL = render_template(
+ "/".join([self.template_path, self._CREATE_SQL]),
+ display_comments=True,
+ program_name=data['jsprname'],
+ program_type=data['jsprtype'],
+ program_action=data['jsprproc']
+ if data['jsprtype'] == 'STORED_PROCEDURE' else
+ data['jsprcode'],
+ number_of_arguments=data['jsprnoofargs'],
+ enabled=data['jsprenabled'],
+ comments=data['jsprdesc'],
+ arguments=data['jsprarguments'] if 'jsprarguments' in data
+ else [],
+ conn=self.conn
+ )
+
+ return ajax_response(response=SQL)
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def get_procedures(self, gid, sid, did, jsid=None):
+ """
+ This function will return procedure list
+ :param gid: group id
+ :param sid: server id
+ :param did: database id
+ :return:
+ """
+ res = []
+ sql = render_template("/".join([self.template_path,
+ 'get_procedures.sql']),
+ datlastsysoid=self._DATABASE_LAST_SYSTEM_OID)
+ status, rset = self.conn.execute_dict(sql)
+ if not status:
+ return internal_server_error(errormsg=rset)
+
+ for row in rset['rows']:
+ # Get formatted Arguments
+ frmtd_params, _ = format_arguments_from_db(
+ self.template_path, self.conn, row)
+
+ res.append({'label': row['proc_name'],
+ 'value': row['proc_name'],
+ 'no_of_args': row['number_of_arguments'],
+ 'arguments': frmtd_params['arguments']
+ })
+
+ return make_json_response(
+ data=res,
+ status=200
+ )
+
+ @check_precondition
+ def enable_disable(self, gid, sid, did, jsid, jsprid=None):
+ """
+ This function is used to enable/disable program.
+ """
+ data = request.form if request.form else json.loads(
+ request.data
+ )
+
+ status, res = self.conn.execute_void(
+ render_template(
+ "/".join([self.template_path, 'enable_disable.sql']),
+ name=data['program_name'],
+ is_enable=data['is_enable_program'], conn=self.conn
+ )
+ )
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ return make_json_response(
+ success=1,
+ info=gettext("Program enabled") if data['is_enable_program'] else
+ gettext('Program disabled'),
+ data={
+ 'sid': sid,
+ 'did': did,
+ 'jsid': jsid,
+ 'jsprid': jsprid
+ }
+ )
+
+
+DBMSProgramView.register_node_view(blueprint)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/static/js/dbms_program.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/static/js/dbms_program.js
new file mode 100644
index 000000000..160e9165f
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/static/js/dbms_program.js
@@ -0,0 +1,189 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2024, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import DBMSProgramSchema from './dbms_program.ui';
+import { getNodeAjaxOptions } from '../../../../../../../static/js/node_ajax';
+import getApiInstance from '../../../../../../../../static/js/api_instance';
+
+define('pgadmin.node.dbms_program', [
+ 'sources/gettext', 'sources/url_for', 'sources/pgadmin',
+ 'pgadmin.browser', 'pgadmin.browser.collection',
+], function(gettext, url_for, pgAdmin, pgBrowser) {
+
+ if (!pgBrowser.Nodes['coll-dbms_program']) {
+ pgBrowser.Nodes['coll-dbms_program'] =
+ pgBrowser.Collection.extend({
+ node: 'dbms_program',
+ label: gettext('DBMS Programs'),
+ type: 'coll-dbms_program',
+ columns: ['jsprname', 'jsprtype', 'jsprenabled', 'jsprdesc'],
+ hasSQL: false,
+ hasDepends: false,
+ hasStatistics: false,
+ hasScriptTypes: [],
+ canDrop: true,
+ canDropCascade: false,
+ });
+ }
+
+ if (!pgBrowser.Nodes['dbms_program']) {
+ pgAdmin.Browser.Nodes['dbms_program'] = pgAdmin.Browser.Node.extend({
+ parent_type: 'dbms_job_scheduler',
+ type: 'dbms_program',
+ label: gettext('DBMS Program'),
+ node_image: 'icon-pga_jobstep',
+ epasHelp: true,
+ epasURL: 'https://www.enterprisedb.com/docs/epas/$VERSION$/epas_compat_bip_guide/03_built-in_packages/15_dbms_scheduler/03_create_program/',
+ dialogHelp: url_for('help.static', {'filename': 'dbms_program.html'}),
+ canDrop: true,
+ hasSQL: true,
+ hasDepends: false,
+ hasStatistics: false,
+ Init: function() {
+ /* Avoid multiple registration of menus */
+ if (this.initialized)
+ return;
+
+ this.initialized = true;
+
+ pgBrowser.add_menus([{
+ name: 'create_dbms_program_on_coll', node: 'coll-dbms_program', module: this,
+ applies: ['object', 'context'], callback: 'show_obj_properties',
+ category: 'create', priority: 4, label: gettext('DBMS Program...'),
+ data: {action: 'create'},
+ }, {
+ name: 'create_dbms_program', node: 'dbms_program', module: this,
+ applies: ['object', 'context'], callback: 'show_obj_properties',
+ category: 'create', priority: 4, label: gettext('DBMS Program...'),
+ data: {action: 'create'},
+ }, {
+ name: 'create_dbms_program', node: 'dbms_job_scheduler', module: this,
+ applies: ['object', 'context'], callback: 'show_obj_properties',
+ category: 'create', priority: 4, label: gettext('DBMS Program...'),
+ data: {action: 'create'},
+ }, {
+ name: 'enable_program', node: 'dbms_program', module: this,
+ applies: ['object', 'context'], callback: 'enable_program',
+ priority: 4, label: gettext('Enable Program'),
+ enable : 'is_enabled',data: {
+ data_disabled: gettext('Program is already enabled.'),
+ },
+ }, {
+ name: 'disable_program', node: 'dbms_program', module: this,
+ applies: ['object', 'context'], callback: 'disable_program',
+ priority: 4, label: gettext('Disable Program'),
+ enable : 'is_disabled',data: {
+ data_disabled: gettext('Program is already disabled.'),
+ },
+ }
+ ]);
+ },
+ is_enabled: function(node) {
+ return !node?.is_enabled;
+ },
+ is_disabled: function(node) {
+ return node?.is_enabled;
+ },
+ callbacks: {
+ enable_program: function(args, notify) {
+ let input = args || {},
+ obj = this,
+ t = pgBrowser.tree,
+ i = 'item' in input ? input.item : t.selected(),
+ d = i ? t.itemData(i) : undefined;
+
+ if (d) {
+ notify = notify || _.isUndefined(notify) || _.isNull(notify);
+ let enable = function() {
+ let data = d;
+ getApiInstance().put(
+ obj.generate_url(i, 'enable_disable', d, true),
+ {'program_name': data.label, 'is_enable_program': true}
+ ).then(({data: res})=> {
+ if (res.success == 1) {
+ pgAdmin.Browser.notifier.success(res.info);
+ t.removeIcon(i);
+ data.icon = 'icon-pga_jobstep';
+ data.is_enabled = true;
+ t.addIcon(i, {icon: data.icon});
+ t.updateAndReselectNode(i, data);
+ }
+ }).catch(function(error) {
+ pgAdmin.Browser.notifier.pgRespErrorNotify(error);
+ t.refresh(i);
+ });
+ };
+ if (notify) {
+ pgAdmin.Browser.notifier.confirm(
+ gettext('Enable Program'),
+ gettext('Are you sure you want to enable the program %s?', d.label),
+ function() { enable(); },
+ function() { return true;},
+ );
+ } else {
+ enable();
+ }
+ }
+
+ return false;
+ },
+ disable_program: function(args, notify) {
+ let input = args || {},
+ obj = this,
+ t = pgBrowser.tree,
+ i = 'item' in input ? input.item : t.selected(),
+ d = i ? t.itemData(i) : undefined;
+
+ if (d) {
+ notify = notify || _.isUndefined(notify) || _.isNull(notify);
+ let disable = function() {
+ let data = d;
+ getApiInstance().put(
+ obj.generate_url(i, 'enable_disable', d, true),
+ {'program_name': data.label, 'is_enable_program': false}
+ ).then(({data: res})=> {
+ if (res.success == 1) {
+ pgAdmin.Browser.notifier.success(res.info);
+ t.removeIcon(i);
+ data.icon = 'icon-pga_jobstep-disabled';
+ data.is_enabled = false;
+ t.addIcon(i, {icon: data.icon});
+ t.updateAndReselectNode(i, data);
+ }
+ }).catch(function(error) {
+ pgAdmin.Browser.notifier.pgRespErrorNotify(error);
+ t.refresh(i);
+ });
+ };
+ if (notify) {
+ pgAdmin.Browser.notifier.confirm(
+ gettext('Disable Program'),
+ gettext('Are you sure you want to disable the program %s?', d.label),
+ function() { disable(); },
+ function() { return true;},
+ );
+ } else {
+ disable();
+ }
+ }
+ return false;
+ },
+ },
+ getSchema: function(treeNodeInfo, itemNodeData) {
+ return new DBMSProgramSchema(
+ {
+ procedures: ()=>getNodeAjaxOptions('get_procedures', this, treeNodeInfo, itemNodeData),
+ }
+ );
+ },
+ });
+ }
+
+ return pgBrowser.Nodes['dbms_program'];
+});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/static/js/dbms_program.ui.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/static/js/dbms_program.ui.js
new file mode 100644
index 000000000..b32ec6415
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/static/js/dbms_program.ui.js
@@ -0,0 +1,76 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2024, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from 'sources/gettext';
+import BaseUISchema from 'sources/SchemaView/base_schema.ui';
+import { isEmptyString } from 'sources/validators';
+import { getActionSchema } from '../../../static/js/dbms_job_scheduler_common.ui';
+
+export default class DBMSProgramSchema extends BaseUISchema {
+ constructor(fieldOptions={}) {
+ super({
+ jsprid: null,
+ jsprname: '',
+ jsprtype: 'PLSQL_BLOCK',
+ jsprenabled: true,
+ jsprnoofargs: 0,
+ jsprarguments: [],
+ jsprdesc: '',
+ jsprproc: null,
+ jsprcode: null,
+ });
+ this.fieldOptions = {
+ procedures: [],
+ ...fieldOptions,
+ };
+ }
+
+ get idAttribute() {
+ return 'jsprid';
+ }
+
+ get baseFields() {
+ let obj = this;
+ return [
+ {
+ id: 'jsprid', label: gettext('ID'), type: 'int', mode: ['properties'],
+ readonly: function(state) {return !obj.isNew(state); },
+ }, {
+ id: 'jsprname', label: gettext('Name'), cell: 'text',
+ type: 'text', noEmpty: true,
+ readonly: function(state) {return !obj.isNew(state); },
+ }, {
+ id: 'jsprenabled', label: gettext('Enabled?'), type: 'switch', cell: 'switch',
+ readonly: function(state) {return !obj.isNew(state); },
+ },
+ // Add the Action Schema
+ ...getActionSchema(obj, 'program'),
+ {
+ id: 'jsprdesc', label: gettext('Comment'), type: 'multiline',
+ readonly: function(state) {return !obj.isNew(state); },
+ }
+ ];
+ }
+ validate(state, setError) {
+ /* code validation*/
+ if (state.jsprtype == 'PLSQL_BLOCK' && isEmptyString(state.jsprcode)) {
+ setError('jsprcode', gettext('Code cannot be empty.'));
+ return true;
+ } else {
+ setError('jsprcode', null);
+ }
+
+ if (state.jsprtype == 'STORED_PROCEDURE' && isEmptyString(state.jsprproc)) {
+ setError('jsprproc', gettext('Procedure cannot be empty.'));
+ return true;
+ } else {
+ setError('jsprproc', null);
+ }
+ }
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/create.sql
new file mode 100644
index 000000000..b77843538
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/create.sql
@@ -0,0 +1,34 @@
+{% if display_comments %}
+-- DBMS Program: '{{ program_name }}'
+
+-- EXEC dbms_scheduler.DROP_PROGRAM('{{ program_name }}');
+
+{% endif %}
+EXEC dbms_scheduler.CREATE_PROGRAM(
+ program_name => {{ program_name|qtLiteral(conn) }},
+ program_type => {{ program_type|qtLiteral(conn) }},
+ program_action => {{ program_action|qtLiteral(conn) }}{% if number_of_arguments or enabled or comments %},{% endif %}
+{% if number_of_arguments %}
+
+ number_of_arguments => {{ number_of_arguments }}{% if enabled or comments %},{% endif %}
+{% endif %}
+{% if enabled %}
+
+ enabled => {{ enabled }}{% if comments %},{% endif %}
+{% endif %}
+{% if comments %}
+
+ comments => {{ comments|qtLiteral(conn) }}
+{% endif %}
+);
+
+{% for args_list_item in arguments %}
+EXEC dbms_scheduler.DEFINE_PROGRAM_ARGUMENT(
+ program_name => {{ program_name|qtLiteral(conn) }},
+ argument_position => {{ args_list_item['argid'] }},
+ argument_name => {{ args_list_item['argname']|qtLiteral(conn) }},
+ argument_type => {{ args_list_item['argtype']|qtLiteral(conn) }},
+ default_value => {{ args_list_item['argdefval']|qtLiteral(conn) }}
+);
+
+{% endfor %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/delete.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/delete.sql
new file mode 100644
index 000000000..83d61ca47
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/delete.sql
@@ -0,0 +1,3 @@
+EXEC dbms_scheduler.DROP_PROGRAM(
+ {{ program_name|qtLiteral(conn) }}
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/enable_disable.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/enable_disable.sql
new file mode 100644
index 000000000..ee43653ee
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/enable_disable.sql
@@ -0,0 +1,5 @@
+{% if is_enable %}
+EXEC dbms_scheduler.ENABLE({{ name|qtLiteral(conn) }});
+{% else %}
+EXEC dbms_scheduler.DISABLE({{ name|qtLiteral(conn) }});
+{% endif %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/get_procedures.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/get_procedures.sql
new file mode 100644
index 000000000..07ff4e18d
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/get_procedures.sql
@@ -0,0 +1,20 @@
+SELECT
+ pg_catalog.concat(pg_catalog.quote_ident(nsp.nspname),'.',pg_catalog.quote_ident(pr.proname)) AS proc_name,
+ pr.pronargs AS number_of_arguments, proargnames,
+ pg_catalog.oidvectortypes(proargtypes) AS proargtypenames,
+ pg_catalog.pg_get_expr(proargdefaults, 'pg_catalog.pg_class'::regclass) AS proargdefaultvals
+FROM
+ pg_catalog.pg_proc pr
+JOIN
+ pg_catalog.pg_type typ ON typ.oid=prorettype
+JOIN
+ pg_catalog.pg_namespace nsp ON nsp.oid=pr.pronamespace
+WHERE
+ pr.prokind IN ('f', 'p')
+ AND typname NOT IN ('trigger', 'event_trigger')
+ AND (pronamespace = 2200::oid OR pronamespace > {{datlastsysoid}}::OID)
+{% if without_args %}
+ AND pr.pronargs = 0
+{% endif %}
+ORDER BY
+ proname;
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/get_program_id.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/get_program_id.sql
new file mode 100644
index 000000000..9e316fcbc
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/get_program_id.sql
@@ -0,0 +1,5 @@
+SELECT
+ dsp_program_id AS jsprid, dsp_program_name AS jsprname,
+ dsp_enabled AS jsprenabled
+FROM sys.scheduler_0200_program
+WHERE dsp_program_name={{ jsprname|qtLiteral(conn) }}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/nodes.sql
new file mode 100644
index 000000000..a22c9253c
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/nodes.sql
@@ -0,0 +1,6 @@
+SELECT
+ dsp_program_id AS jsprid, dsp_program_name AS jsprname,
+ dsp_program_type AS jsprtype, dsp_enabled AS jsprenabled,
+ dsp_comments AS jsprdesc
+FROM sys.scheduler_0200_program prt
+ JOIN sys.dba_scheduler_programs prv ON prt.dsp_program_name = prv.program_name
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/properties.sql
new file mode 100644
index 000000000..58bcdf6bb
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/properties.sql
@@ -0,0 +1,18 @@
+SELECT
+ dsp_program_id AS jsprid, dsp_program_name AS jsprname,
+ dsp_program_type AS jsprtype,
+ CASE WHEN dsp_program_type = 'PLSQL_BLOCK' THEN dsp_program_action ELSE '' END AS jsprcode,
+ CASE WHEN dsp_program_type = 'STORED_PROCEDURE' THEN dsp_program_action ELSE '' END AS jsprproc,
+ dsp_number_of_arguments AS jsprnoofargs, dsp_enabled AS jsprenabled,
+ dsp_comments AS jsprdesc, array_agg(argument_name) AS proargnames,
+ array_agg(argument_type) AS proargtypenames,
+ array_agg(default_value) AS proargdefaultvals
+FROM sys.scheduler_0200_program prt
+{% if not jsprid %}
+ JOIN sys.dba_scheduler_programs prv ON prt.dsp_program_name = prv.program_name
+{% endif %}
+ LEFT JOIN dba_scheduler_program_args prargs ON dsp_program_name = prargs.program_name
+{% if jsprid %}
+WHERE dsp_program_id={{jsprid}}::oid
+{% endif %}
+GROUP BY dsp_program_id, dsp_program_name
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/dbms_programs_test_data.json b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/dbms_programs_test_data.json
new file mode 100644
index 000000000..32f884627
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/dbms_programs_test_data.json
@@ -0,0 +1,269 @@
+{
+ "dbms_create_program": [
+ {
+ "name": "Create program when type is PLSQL_BLOCK",
+ "url": "/browser/dbms_program/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsprid": null,
+ "jsprname": "prg_with_psql",
+ "jsprtype": "PLSQL_BLOCK",
+ "jsprenabled": true,
+ "jsprnoofargs": 0,
+ "jsprarguments": [],
+ "jsprdesc": "This is a PLSQL program.",
+ "jsprproc": null,
+ "jsprcode": "BEGIN PERFORM 1; END;"
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create program when type is STORED_PROCEDURE with args",
+ "url": "/browser/dbms_program/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsprid": null,
+ "jsprname": "prg_with_proc_args",
+ "jsprtype": "STORED_PROCEDURE",
+ "jsprenabled": true,
+ "jsprnoofargs": 2,
+ "jsprarguments": [{"argid":0,"argtype":"bigint","argmode":"IN","argname":"salary","argdefval":"10000"},{"argid":1,"argtype":"character varying","argmode":"IN","argname":"name","argdefval":" -"}],
+ "jsprdesc": "This is a STORED_PROCEDURE program.",
+ "jsprproc": "public.test_proc_with_args",
+ "jsprcode": null
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create program when type is STORED_PROCEDURE without args",
+ "url": "/browser/dbms_program/obj/",
+ "proc_name": "public.test_proc_without_args()",
+ "is_positive_test": true,
+ "test_data": {
+ "jsprid": null,
+ "jsprname": "prg_with_proc_without_args",
+ "jsprtype": "STORED_PROCEDURE",
+ "jsprenabled": false,
+ "jsprnoofargs": 0,
+ "jsprarguments": [],
+ "jsprdesc": "This is a STORED_PROCEDURE program.",
+ "jsprproc": "public.test_proc_without_args",
+ "jsprcode": null
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create program: while server is down",
+ "url": "/browser/dbms_program/obj/",
+ "is_positive_test": false,
+ "test_data": {
+ "jsprid": null,
+ "jsprname": "prg_with_psql",
+ "jsprtype": "PLSQL_BLOCK",
+ "jsprenabled": true,
+ "jsprnoofargs": 0,
+ "jsprarguments": [],
+ "jsprdesc": "This is a PLSQL program.",
+ "jsprproc": null,
+ "jsprcode": "BEGIN PERFORM 1; END;"
+ },
+ "mocking_required": true,
+ "mock_data": {
+ "function_name": "pgadmin.utils.driver.psycopg3.connection.Connection.execute_scalar",
+ "return_value": "[(False,'Mocked Internal Server Error')]"
+ },
+ "expected_data": {
+ "status_code": 500,
+ "error_msg": "Mocked Internal Server Error",
+ "test_result_data": {}
+ }
+ }
+ ],
+ "dbms_delete_program": [
+ {
+ "name": "Delete program: With existing DBMS program.",
+ "url": "/browser/dbms_program/obj/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": false
+ },
+ {
+ "name": "Delete multiple programs: With existing DBMS programs.",
+ "url": "/browser/dbms_program/obj/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": true
+ }
+ ],
+ "dbms_get_program": [
+ {
+ "name": "Get program: With existing DBMS program.",
+ "url": "/browser/dbms_program/obj/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": false
+ },
+ {
+ "name": "Get programs: With multiple existing DBMS programs.",
+ "url": "/browser/dbms_program/obj/",
+ "proc_name": "public.test_proc_with_args(IN salary bigint DEFAULT 10000, IN name character varying)",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": true
+ },
+ {
+ "name": "Get program: while server down.",
+ "url": "/browser/dbms_program/obj/",
+ "is_positive_test": false,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": true,
+ "mock_data": {
+ "function_name": "pgadmin.utils.driver.psycopg3.connection.Connection.execute_dict",
+ "return_value": "(False,'Mocked Internal Server Error')"
+ },
+ "expected_data": {
+ "status_code": 500,
+ "error_msg": "Mocked Internal Server Error",
+ "test_result_data": {}
+ },
+ "is_list": false
+ }
+ ],
+ "dbms_msql_program": [
+ {
+ "name": "Get program msql: For existing PLSQL program.",
+ "url": "/browser/dbms_program/msql/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {
+ "jsprid": null,
+ "jsprname": "prg_with_psql",
+ "jsprtype": "PLSQL_BLOCK",
+ "jsprenabled": true,
+ "jsprnoofargs": 0,
+ "jsprarguments": [],
+ "jsprdesc": "This is a PLSQL program.",
+ "jsprproc": null,
+ "jsprcode": "BEGIN PERFORM 1; END;"
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": false
+ },
+ {
+ "name": "Get program msql: For existing STORED_PROCEDURE program.",
+ "url": "/browser/dbms_program/msql/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {
+ "jsprid": null,
+ "jsprname": "prg_with_proc_args",
+ "jsprtype": "STORED_PROCEDURE",
+ "jsprenabled": true,
+ "jsprnoofargs": 2,
+ "jsprarguments": "[{\"argid\":0,\"argtype\":\"bigint\",\"argmode\":\"IN\",\"argname\":\"salary\",\"argdefval\":\"10000\"},{\"argid\":1,\"argtype\":\"character varying\",\"argmode\":\"IN\",\"argname\":\"name\",\"argdefval\":\" -\"}]",
+ "jsprdesc": "This is a STORED_PROCEDURE program.",
+ "jsprproc": "public.test_proc_with_args",
+ "jsprcode": null
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": false
+ }
+ ],
+ "dbms_enable_program": [
+ {
+ "name": "Enable existing program",
+ "url": "/browser/dbms_program/enable_disable/",
+ "is_positive_test": true,
+ "test_data": {
+ "is_enable_program": true
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ }
+ ],
+ "dbms_disable_program": [
+ {
+ "name": "Disable existing program",
+ "url": "/browser/dbms_program/enable_disable/",
+ "is_positive_test": true,
+ "test_data": {
+ "is_enable_program": false
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ }
+ ]
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_disabled.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_disabled.sql
new file mode 100644
index 000000000..01448feae
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_disabled.sql
@@ -0,0 +1,8 @@
+-- DBMS Program: 'dbms_prg_disabled'
+
+-- EXEC dbms_scheduler.DROP_PROGRAM('dbms_prg_disabled');
+
+EXEC dbms_scheduler.CREATE_PROGRAM(
+ program_name => 'dbms_prg_disabled',
+ program_type => 'PLSQL_BLOCK',
+ program_action => 'BEGIN PERFORM 1; END;');
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_disabled_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_disabled_msql.sql
new file mode 100644
index 000000000..5dd28145b
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_disabled_msql.sql
@@ -0,0 +1,4 @@
+EXEC dbms_scheduler.CREATE_PROGRAM(
+ program_name => 'dbms_prg_disabled',
+ program_type => 'PLSQL_BLOCK',
+ program_action => 'BEGIN PERFORM 1; END;');
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_with_args.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_with_args.sql
new file mode 100644
index 000000000..6f4e6e83f
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_with_args.sql
@@ -0,0 +1,28 @@
+-- DBMS Program: 'dbms_prg_proc_with_args'
+
+-- EXEC dbms_scheduler.DROP_PROGRAM('dbms_prg_proc_with_args');
+
+EXEC dbms_scheduler.CREATE_PROGRAM(
+ program_name => 'dbms_prg_proc_with_args',
+ program_type => 'STORED_PROCEDURE',
+ program_action => 'public.test_proc_with_args',
+ number_of_arguments => 2,
+ enabled => True,
+ comments => 'This is a STORED_PROCEDURE program.'
+);
+
+EXEC dbms_scheduler.DEFINE_PROGRAM_ARGUMENT(
+ program_name => 'dbms_prg_proc_with_args',
+ argument_position => 0,
+ argument_name => 'salary',
+ argument_type => 'bigint',
+ default_value => '10000'
+);
+
+EXEC dbms_scheduler.DEFINE_PROGRAM_ARGUMENT(
+ program_name => 'dbms_prg_proc_with_args',
+ argument_position => 1,
+ argument_name => 'name',
+ argument_type => 'character varying',
+ default_value => ' -'
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_with_args_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_with_args_msql.sql
new file mode 100644
index 000000000..03cf1941e
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_with_args_msql.sql
@@ -0,0 +1,24 @@
+EXEC dbms_scheduler.CREATE_PROGRAM(
+ program_name => 'dbms_prg_proc_with_args',
+ program_type => 'STORED_PROCEDURE',
+ program_action => 'public.test_proc_with_args',
+ number_of_arguments => 2,
+ enabled => True,
+ comments => 'This is a STORED_PROCEDURE program.'
+);
+
+EXEC dbms_scheduler.DEFINE_PROGRAM_ARGUMENT(
+ program_name => 'dbms_prg_proc_with_args',
+ argument_position => 0,
+ argument_name => 'salary',
+ argument_type => 'bigint',
+ default_value => '10000'
+);
+
+EXEC dbms_scheduler.DEFINE_PROGRAM_ARGUMENT(
+ program_name => 'dbms_prg_proc_with_args',
+ argument_position => 1,
+ argument_name => 'name',
+ argument_type => 'character varying',
+ default_value => ' -'
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_without_args.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_without_args.sql
new file mode 100644
index 000000000..d32132e75
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_without_args.sql
@@ -0,0 +1,10 @@
+-- DBMS Program: 'dbms_prg_proc_without_args'
+
+-- EXEC dbms_scheduler.DROP_PROGRAM('dbms_prg_proc_without_args');
+
+EXEC dbms_scheduler.CREATE_PROGRAM(
+ program_name => 'dbms_prg_proc_without_args',
+ program_type => 'STORED_PROCEDURE',
+ program_action => 'public.test_proc_without_args',
+ comments => 'This is a STORED_PROCEDURE program.'
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_without_args_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_without_args_msql.sql
new file mode 100644
index 000000000..43e8a9aa4
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_without_args_msql.sql
@@ -0,0 +1,6 @@
+EXEC dbms_scheduler.CREATE_PROGRAM(
+ program_name => 'dbms_prg_proc_without_args',
+ program_type => 'STORED_PROCEDURE',
+ program_action => 'public.test_proc_without_args',
+ comments => 'This is a STORED_PROCEDURE program.'
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_psql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_psql.sql
new file mode 100644
index 000000000..8344fb59e
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_psql.sql
@@ -0,0 +1,11 @@
+-- DBMS Program: 'dbms_prg_with_psql'
+
+-- EXEC dbms_scheduler.DROP_PROGRAM('dbms_prg_with_psql');
+
+EXEC dbms_scheduler.CREATE_PROGRAM(
+ program_name => 'dbms_prg_with_psql',
+ program_type => 'PLSQL_BLOCK',
+ program_action => 'BEGIN PERFORM 1; END;',
+ enabled => True,
+ comments => 'This is a PLSQL program.'
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_psql_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_psql_msql.sql
new file mode 100644
index 000000000..7bffeba44
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_psql_msql.sql
@@ -0,0 +1,7 @@
+EXEC dbms_scheduler.CREATE_PROGRAM(
+ program_name => 'dbms_prg_with_psql',
+ program_type => 'PLSQL_BLOCK',
+ program_action => 'BEGIN PERFORM 1; END;',
+ enabled => True,
+ comments => 'This is a PLSQL program.'
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/test.json b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/test.json
new file mode 100644
index 000000000..67a872d41
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/test.json
@@ -0,0 +1,145 @@
+{
+ "scenarios": [
+ {
+ "type": "create",
+ "name": "Create extension 'edb_job_scheduler' for DBMS Program",
+ "endpoint": "NODE-extension.obj",
+ "sql_endpoint": "NODE-extension.sql_id",
+ "data": {
+ "name": "edb_job_scheduler"
+ },
+ "store_object_id": true
+ },
+ {
+ "type": "create",
+ "name": "Create extension 'dbms_scheduler' for DBMS Program",
+ "endpoint": "NODE-extension.obj",
+ "sql_endpoint": "NODE-extension.sql_id",
+ "data": {
+ "name": "dbms_scheduler"
+ },
+ "store_object_id": true
+ },
+ {
+ "type": "create",
+ "name": "Create Program with PLSQL_BLOCK",
+ "endpoint": "NODE-dbms_program.obj",
+ "sql_endpoint": "NODE-dbms_program.sql_id",
+ "msql_endpoint": "NODE-dbms_program.msql",
+ "data": {
+ "jsprid": null,
+ "jsprname": "dbms_prg_with_psql",
+ "jsprtype": "PLSQL_BLOCK",
+ "jsprenabled": true,
+ "jsprnoofargs": 0,
+ "jsprarguments": [],
+ "jsprdesc": "This is a PLSQL program.",
+ "jsprproc": null,
+ "jsprcode": "BEGIN PERFORM 1; END;"
+ },
+ "expected_sql_file": "create_program_psql.sql",
+ "expected_msql_file": "create_program_psql_msql.sql"
+ },
+ {
+ "type": "delete",
+ "name": "Drop Program",
+ "endpoint": "NODE-dbms_program.obj_id",
+ "data": {
+ "name": "dbms_prg_with_psql"
+ }
+ },
+ {
+ "type": "create",
+ "name": "Create Program with Stored Procedure without args",
+ "endpoint": "NODE-dbms_program.obj",
+ "sql_endpoint": "NODE-dbms_program.sql_id",
+ "msql_endpoint": "NODE-dbms_program.msql",
+ "data": {
+ "jsprid": null,
+ "jsprname": "dbms_prg_proc_without_args",
+ "jsprtype": "STORED_PROCEDURE",
+ "jsprenabled": false,
+ "jsprnoofargs": 0,
+ "jsprarguments": [],
+ "jsprdesc": "This is a STORED_PROCEDURE program.",
+ "jsprproc": "public.test_proc_without_args",
+ "jsprcode": null
+ },
+ "expected_sql_file": "create_program_proc_without_args.sql",
+ "expected_msql_file": "create_program_proc_without_args_msql.sql"
+ },
+ {
+ "type": "delete",
+ "name": "Drop Program",
+ "endpoint": "NODE-dbms_program.obj_id",
+ "data": {
+ "name": "dbms_prg_proc_without_args"
+ }
+ },
+ {
+ "type": "create",
+ "name": "Create Program with Stored Procedure with args",
+ "endpoint": "NODE-dbms_program.obj",
+ "sql_endpoint": "NODE-dbms_program.sql_id",
+ "msql_endpoint": "NODE-dbms_program.msql",
+ "data": {
+ "jsprid": null,
+ "jsprname": "dbms_prg_proc_with_args",
+ "jsprtype": "STORED_PROCEDURE",
+ "jsprenabled": true,
+ "jsprnoofargs": 2,
+ "jsprarguments": [{"argid":0,"argtype":"bigint","argmode":"IN","argname":"salary","argdefval":"10000"},{"argid":1,"argtype":"character varying","argmode":"IN","argname":"name","argdefval":" -"}],
+ "jsprdesc": "This is a STORED_PROCEDURE program.",
+ "jsprproc": "public.test_proc_with_args",
+ "jsprcode": null
+ },
+ "expected_sql_file": "create_program_proc_with_args.sql",
+ "expected_msql_file": "create_program_proc_with_args_msql.sql"
+ },
+ {
+ "type": "delete",
+ "name": "Drop Program",
+ "endpoint": "NODE-dbms_program.obj_id",
+ "data": {
+ "name": "dbms_prg_proc_with_args"
+ }
+ },
+ {
+ "type": "create",
+ "name": "Create disabled program",
+ "endpoint": "NODE-dbms_program.obj",
+ "sql_endpoint": "NODE-dbms_program.sql_id",
+ "msql_endpoint": "NODE-dbms_program.msql",
+ "data": {
+ "jsprid": null,
+ "jsprname": "dbms_prg_disabled",
+ "jsprtype": "PLSQL_BLOCK",
+ "jsprenabled": false,
+ "jsprnoofargs": 0,
+ "jsprarguments": [],
+ "jsprdesc": "",
+ "jsprproc": null,
+ "jsprcode": "BEGIN PERFORM 1; END;"
+ },
+ "expected_sql_file": "create_program_disabled.sql",
+ "expected_msql_file": "create_program_disabled_msql.sql"
+ },
+ {
+ "type": "delete",
+ "name": "Drop Program",
+ "endpoint": "NODE-dbms_program.obj_id",
+ "data": {
+ "name": "dbms_prg_disabled"
+ }
+ },
+ {
+ "type": "delete",
+ "name": "Drop Extension",
+ "endpoint": "NODE-extension.delete",
+ "data": {
+ "ids": [""]
+ },
+ "preprocess_data": true
+ }
+ ]
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_add_program.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_add_program.py
new file mode 100644
index 000000000..73ca2b964
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_add_program.py
@@ -0,0 +1,83 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import os
+import json
+from unittest.mock import patch
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_programs_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSAddProgramTestCase(BaseTestGenerator):
+ """This class will test the add program in the DBMS Program API"""
+ scenarios = utils.generate_scenarios("dbms_create_program",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ def runTest(self):
+ """ This function will add DBMS Program under test database. """
+ if self.is_positive_test:
+ response = job_scheduler_utils.api_create(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ # Verify in backend
+ response_data = json.loads(response.data)
+ self.programs_id = response_data['node']['_id']
+ programs_name = response_data['node']['label']
+ is_present = job_scheduler_utils.verify_dbms_program(
+ self, programs_name)
+ self.assertTrue(
+ is_present,"DBMS program was not created successfully.")
+ else:
+ if self.mocking_required:
+ with patch(self.mock_data["function_name"],
+ side_effect=eval(self.mock_data["return_value"])):
+ response = job_scheduler_utils.api_create(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+ utils.assert_error_message(self, response)
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_delete_program.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_delete_program.py
new file mode 100644
index 000000000..19928dd00
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_delete_program.py
@@ -0,0 +1,98 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import uuid
+import os
+import json
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_programs_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSDeleteProgramTestCase(BaseTestGenerator):
+ """This class will test the add program in the DBMS program API"""
+ scenarios = utils.generate_scenarios("dbms_delete_program",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ self.prg_name = "test_program_delete%s" % str(uuid.uuid4())[1:8]
+ self.program_id = job_scheduler_utils.create_dbms_program(
+ self, self.prg_name)
+
+ # multiple programs
+ if self.is_list:
+ self.prg_name2 = "test_program_delete%s" % str(uuid.uuid4())[1:8]
+ self.program_id_2 = job_scheduler_utils.create_dbms_program(
+ self, self.prg_name2)
+
+ def runTest(self):
+ """
+ This function will test delete DBMS program under test database.
+ """
+ if self.is_list:
+ self.data['ids'] = [self.program_id, self.program_id_2]
+ response = job_scheduler_utils.api_delete(self, '')
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ is_present = job_scheduler_utils.verify_dbms_program(
+ self, self.prg_name)
+ self.assertFalse(
+ is_present, "DBMS program was not deleted successfully")
+
+ is_present = job_scheduler_utils.verify_dbms_program(
+ self, self.prg_name2)
+ self.assertFalse(
+ is_present, "DBMS program was not deleted successfully")
+ else:
+ response = job_scheduler_utils.api_delete(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ is_present = job_scheduler_utils.verify_dbms_program(
+ self, self.prg_name)
+ self.assertFalse(
+ is_present, "DBMS program was not deleted successfully")
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_disable_program.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_disable_program.py
new file mode 100644
index 000000000..610dcbef2
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_disable_program.py
@@ -0,0 +1,71 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import uuid
+import os
+import json
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_programs_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSDisableProgramTestCase(BaseTestGenerator):
+ """This class will test the add program in the DBMS program API"""
+ scenarios = utils.generate_scenarios("dbms_disable_program",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ self.prg_name = "test_program_disable%s" % str(uuid.uuid4())[1:8]
+ self.data['program_name'] = self.prg_name
+ self.program_id = job_scheduler_utils.create_dbms_program(
+ self, self.prg_name)
+
+ def runTest(self):
+ """ This function will test DBMS program under test database."""
+ response = job_scheduler_utils.api_put(self, self.program_id)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.delete_dbms_program(self, self.prg_name)
+
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_enable_program.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_enable_program.py
new file mode 100644
index 000000000..9ce5b1762
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_enable_program.py
@@ -0,0 +1,71 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import uuid
+import os
+import json
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_programs_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSEnableProgramTestCase(BaseTestGenerator):
+ """This class will test the enable program in the DBMS program API"""
+ scenarios = utils.generate_scenarios("dbms_enable_program",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ self.prg_name = "test_program_enable%s" % str(uuid.uuid4())[1:8]
+ self.data['program_name'] = self.prg_name
+ self.program_id = job_scheduler_utils.create_dbms_program(
+ self, self.prg_name, False)
+
+ def runTest(self):
+ """ This function will test DBMS program under test database."""
+ response = job_scheduler_utils.api_put(self, self.program_id)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.delete_dbms_program(self, self.prg_name)
+
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_get_msql_program.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_get_msql_program.py
new file mode 100644
index 000000000..9c6a050c6
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_get_msql_program.py
@@ -0,0 +1,65 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import os
+import json
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_programs_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSGetMSQLProgramTestCase(BaseTestGenerator):
+ """This class will test the add program in the DBMS program API"""
+ scenarios = utils.generate_scenarios("dbms_msql_program",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ def runTest(self):
+ """ This function will add DBMS program under test database. """
+ url_encode_data = self.data
+
+ response = job_scheduler_utils.api_get_msql(self, url_encode_data)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_get_program.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_get_program.py
new file mode 100644
index 000000000..76823d70b
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_get_program.py
@@ -0,0 +1,92 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import uuid
+import os
+import json
+from unittest.mock import patch
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_programs_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSGetProgramTestCase(BaseTestGenerator):
+ """This class will test the add program in the DBMS program API"""
+ scenarios = utils.generate_scenarios("dbms_get_program",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ self.prg_name = "test_program_get%s" % str(uuid.uuid4())[1:8]
+ self.program_id = job_scheduler_utils.create_dbms_program(
+ self, self.prg_name)
+
+ # multiple programs
+ if self.is_list:
+ self.prg_name2 = "test_program_get%s" % str(uuid.uuid4())[1:8]
+ self.program_id_2 = job_scheduler_utils.create_dbms_program(
+ self, self.prg_name2, True, True, self.proc_name)
+
+ def runTest(self):
+ """ This function will test DBMS program under test database."""
+ if self.is_positive_test:
+ if self.is_list:
+ response = job_scheduler_utils.api_get(self, '')
+ else:
+ response = job_scheduler_utils.api_get(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+ else:
+ if self.mocking_required:
+ with patch(self.mock_data["function_name"],
+ side_effect=[eval(self.mock_data["return_value"])]):
+ response = job_scheduler_utils.api_get(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+ utils.assert_error_message(self, response)
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.delete_dbms_program(self, self.prg_name)
+ if self.is_list:
+ job_scheduler_utils.delete_dbms_program(self, self.prg_name2)
+
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/__init__.py
new file mode 100644
index 000000000..00be42333
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/__init__.py
@@ -0,0 +1,508 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+""" Implements DBMS Schedule objects Node."""
+
+import json
+from functools import wraps
+
+from flask import render_template, request, jsonify
+from flask_babel import gettext
+from pgadmin.browser.collection import CollectionNodeModule
+from pgadmin.browser.server_groups.servers import databases
+from pgadmin.browser.utils import PGChildNodeView
+from pgadmin.utils.ajax import make_json_response, gone, \
+ make_response as ajax_response, internal_server_error
+from pgadmin.utils.driver import get_driver
+from config import PG_DEFAULT_DRIVER
+from pgadmin.utils.constants import DBMS_JOB_SCHEDULER_ID
+from pgadmin.browser.server_groups.servers.databases.dbms_job_scheduler.utils \
+ import resolve_calendar_string, create_calendar_string
+
+
+class DBMSScheduleModule(CollectionNodeModule):
+ """
+ class DBMSScheduleModule(CollectionNodeModule)
+
+ A module class for DBMS Schedule objects node derived
+ from CollectionNodeModule.
+
+ Methods:
+ -------
+
+ * get_nodes(gid, sid, did)
+ - Method is used to generate the browser collection node.
+
+ * script_load()
+ - Load the module script for DBMS Schedule objects, when any of
+ the server node is initialized.
+ """
+ _NODE_TYPE = 'dbms_schedule'
+ _COLLECTION_LABEL = gettext("DBMS Schedules")
+
+ @property
+ def collection_icon(self):
+ """
+ icon to be displayed for the browser collection node
+ """
+ return 'icon-coll-pga_schedule'
+
+ @property
+ def node_icon(self):
+ """
+ icon to be displayed for the browser nodes
+ """
+ return 'icon-pga_schedule'
+
+ def get_nodes(self, gid, sid, did, jsid):
+ """
+ Generate the collection node
+ """
+ if self.show_node:
+ 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 server, when any of the database node is
+ initialized.
+ """
+ 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 = DBMSScheduleModule(__name__)
+
+
+class DBMSScheduleView(PGChildNodeView):
+ """
+ class DBMSScheduleView(PGChildNodeView)
+
+ A view class for DBMSSchedule node derived from PGChildNodeView.
+ This class is responsible for all the stuff related to view like
+ updating schedule node, showing properties, showing sql in sql pane.
+
+ Methods:
+ -------
+ * __init__(**kwargs)
+ - Method is used to initialize the DBMSScheduleView, 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 schedule nodes within that
+ collection.
+
+ * nodes()
+ - This function will use to create all the child node within that
+ collection. Here it will create all the schedule node.
+
+ * properties(gid, sid, did, jsid, jsscid)
+ - This function will show the properties of the selected schedule node
+
+ * create(gid, sid, did, jsid, jsscid)
+ - This function will create the new schedule object
+
+ * msql(gid, sid, did, jsid, jsscid)
+ - This function is used to return modified SQL for the
+ selected schedule node
+
+ * sql(gid, sid, did, jsid, jsscid)
+ - Dummy response for sql panel
+
+ * delete(gid, sid, did, jsid, jsscid)
+ - Drops job schedule
+ """
+
+ node_type = blueprint.node_type
+ BASE_TEMPLATE_PATH = 'dbms_schedules/ppas/#{0}#'
+
+ parent_ids = [
+ {'type': 'int', 'id': 'gid'},
+ {'type': 'int', 'id': 'sid'},
+ {'type': 'int', 'id': 'did'},
+ {'type': 'int', 'id': 'jsid'}
+ ]
+ ids = [
+ {'type': 'int', 'id': 'jsscid'}
+ ]
+
+ operations = dict({
+ 'obj': [
+ {'get': 'properties', 'delete': 'delete'},
+ {'get': 'list', 'post': 'create', 'delete': 'delete'}
+ ],
+ 'nodes': [{'get': 'nodes'}, {'get': 'nodes'}],
+ 'msql': [{'get': 'msql'}, {'get': 'msql'}],
+ 'sql': [{'get': 'sql'}]
+ })
+
+ def _init_(self, **kwargs):
+ self.conn = None
+ self.template_path = None
+ self.manager = None
+
+ super().__init__(**kwargs)
+
+ def check_precondition(f):
+ """
+ This function will behave as a decorator which will check the
+ database connection before running view. It will also attach
+ 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'])
+ # Set the template path for the SQL scripts
+ self.template_path = self.BASE_TEMPLATE_PATH.format(
+ self.manager.version)
+
+ return f(*args, **kwargs)
+
+ return wrap
+
+ @check_precondition
+ def list(self, gid, sid, did, jsid):
+ """
+ This function is used to list all the schedule nodes within
+ that collection.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler 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, jsid, jsscid=None):
+ """
+ This function is used to create all the child nodes within
+ the collection. Here it will create all the schedule nodes.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler ID
+ """
+ res = []
+ try:
+ sql = render_template(
+ "/".join([self.template_path, self._NODES_SQL]))
+
+ status, result = self.conn.execute_2darray(sql)
+ if not status:
+ return internal_server_error(errormsg=result)
+
+ if jsscid is not None:
+ if len(result['rows']) == 0:
+ return gone(
+ errormsg=gettext("Could not find the specified "
+ "schedule.")
+ )
+
+ row = result['rows'][0]
+ return make_json_response(
+ data=self.blueprint.generate_browser_node(
+ row['jsscid'],
+ DBMS_JOB_SCHEDULER_ID,
+ row['jsscname'],
+ icon="icon-pga_schedule",
+ description=row['jsscdesc']
+ )
+ )
+
+ for row in result['rows']:
+ res.append(
+ self.blueprint.generate_browser_node(
+ row['jsscid'],
+ DBMS_JOB_SCHEDULER_ID,
+ row['jsscname'],
+ icon="icon-pga_schedule",
+ description=row['jsscdesc']
+ )
+ )
+
+ return make_json_response(
+ data=res,
+ status=200
+ )
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def properties(self, gid, sid, did, jsid, jsscid):
+ """
+ This function will show the properties of the selected schedule node.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler ID
+ jsscid: JobSchedule ID
+ """
+ try:
+ sql = render_template(
+ "/".join([self.template_path, self._PROPERTIES_SQL]),
+ jsscid=jsscid
+ )
+ status, res = self.conn.execute_dict(sql)
+
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ if len(res['rows']) == 0:
+ return gone(
+ errormsg=gettext("Could not find the specified schedule.")
+ )
+
+ # Resolve the repeat interval string
+ if 'jsscrepeatint' in res['rows'][0]:
+ (freq, by_date, by_month, by_month_day, by_weekday, by_hour,
+ by_minute) = resolve_calendar_string(
+ res['rows'][0]['jsscrepeatint'])
+
+ res['rows'][0]['jsscfreq'] = freq
+ res['rows'][0]['jsscdate'] = by_date
+ res['rows'][0]['jsscmonths'] = by_month
+ res['rows'][0]['jsscmonthdays'] = by_month_day
+ res['rows'][0]['jsscweekdays'] = by_weekday
+ res['rows'][0]['jsschours'] = by_hour
+ res['rows'][0]['jsscminutes'] = by_minute
+
+ return ajax_response(
+ response=res['rows'][0],
+ status=200
+ )
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def create(self, gid, sid, did, jsid):
+ """
+ This function will create the schedule node.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler ID
+ """
+ data = json.loads(request.data)
+ try:
+ # Create calendar string for repeat interval
+ repeat_interval = create_calendar_string(
+ data['jsscfreq'], data['jsscdate'], data['jsscmonths'],
+ data['jsscmonthdays'], data['jsscweekdays'], data['jsschours'],
+ data['jsscminutes'])
+
+ sql = render_template(
+ "/".join([self.template_path, self._CREATE_SQL]),
+ schedule_name=data['jsscname'],
+ start_date=data['jsscstart'],
+ repeat_interval=repeat_interval,
+ end_date=data['jsscend'],
+ comments=data['jsscdesc'],
+ conn=self.conn
+ )
+
+ status, res = self.conn.execute_void('BEGIN')
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ status, res = self.conn.execute_scalar(sql)
+ if not status:
+ if self.conn.connected():
+ self.conn.execute_void('END')
+ return internal_server_error(errormsg=res)
+
+ self.conn.execute_void('END')
+
+ # Get the newly created Schedule id
+ sql = render_template(
+ "/".join([self.template_path, 'get_schedule_id.sql']),
+ jsscname=data['jsscname'], conn=self.conn
+ )
+ status, res = self.conn.execute_dict(sql)
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ if len(res['rows']) == 0:
+ return gone(
+ errormsg=gettext("Job schedule creation failed.")
+ )
+ row = res['rows'][0]
+
+ return jsonify(
+ node=self.blueprint.generate_browser_node(
+ row['jsscid'],
+ DBMS_JOB_SCHEDULER_ID,
+ row['jsscname'],
+ icon="icon-pga_schedule"
+ )
+ )
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def delete(self, gid, sid, did, jsid, jsscid=None):
+ """Delete the Job Schedule."""
+
+ if jsscid is None:
+ data = request.form if request.form else json.loads(
+ request.data
+ )
+ else:
+ data = {'ids': [jsscid]}
+
+ try:
+ for jsscid in data['ids']:
+ sql = render_template(
+ "/".join([self.template_path, self._PROPERTIES_SQL]),
+ jsscid=jsscid
+ )
+
+ status, res = self.conn.execute_dict(sql)
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ jsscname = res['rows'][0]['jsscname']
+
+ status, res = self.conn.execute_void(
+ render_template(
+ "/".join([self.template_path, self._DELETE_SQL]),
+ schedule_name=jsscname, force=False, conn=self.conn
+ )
+ )
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ return make_json_response(success=1)
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def msql(self, gid, sid, did, jsid, jsscid=None):
+ """
+ This function is used to return modified SQL for the
+ selected Schedule node.
+
+ Args:
+ gid: Server Group ID
+ sid: Server ID
+ jsid: Job Scheduler ID
+ jsscid: Job Schedule ID (optional)
+ """
+ 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 ('jsscdesc',):
+ data[k] = v
+ else:
+ data[k] = json.loads(v)
+ except ValueError:
+ data[k] = v
+
+ try:
+ # Create calendar string for repeat interval
+ repeat_interval = create_calendar_string(
+ data['jsscfreq'], data['jsscdate'], data['jsscmonths'],
+ data['jsscmonthdays'], data['jsscweekdays'], data['jsschours'],
+ data['jsscminutes'])
+
+ sql = render_template(
+ "/".join([self.template_path, self._CREATE_SQL]),
+ schedule_name=data['jsscname'],
+ start_date=data['jsscstart'],
+ repeat_interval=repeat_interval,
+ end_date=data['jsscend'],
+ comments=data['jsscdesc'],
+ conn=self.conn
+ )
+
+ return make_json_response(
+ data=sql,
+ status=200
+ )
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+ @check_precondition
+ def sql(self, gid, sid, did, jsid, jsscid):
+ """
+ This function will generate sql for the sql panel
+ """
+ try:
+ SQL = render_template("/".join(
+ [self.template_path, self._PROPERTIES_SQL]
+ ), jsscid=jsscid)
+
+ status, res = self.conn.execute_dict(SQL)
+ if not status:
+ return internal_server_error(errormsg=res)
+
+ if len(res['rows']) == 0:
+ return gone(
+ gettext("Could not find the DBMS Schedule.")
+ )
+
+ data = res['rows'][0]
+
+ SQL = render_template(
+ "/".join([self.template_path, self._CREATE_SQL]),
+ display_comments=True,
+ schedule_name=data['jsscname'],
+ start_date=data['jsscstart'],
+ repeat_interval=data['jsscrepeatint'],
+ end_date=data['jsscend'],
+ comments=data['jsscdesc'],
+ conn=self.conn
+ )
+
+ return ajax_response(response=SQL)
+ except Exception as e:
+ return internal_server_error(errormsg=str(e))
+
+
+DBMSScheduleView.register_node_view(blueprint)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/static/js/dbms_schedule.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/static/js/dbms_schedule.js
new file mode 100644
index 000000000..c8ec3c70c
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/static/js/dbms_schedule.js
@@ -0,0 +1,77 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2024, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import DBMSScheduleSchema from './dbms_schedule.ui';
+
+define('pgadmin.node.dbms_schedule', [
+ 'sources/gettext', 'sources/url_for', 'sources/pgadmin',
+ 'pgadmin.browser', 'pgadmin.browser.collection',
+], function(gettext, url_for, pgAdmin, pgBrowser) {
+
+ if (!pgBrowser.Nodes['coll-dbms_schedule']) {
+ pgBrowser.Nodes['coll-dbms_schedule'] =
+ pgBrowser.Collection.extend({
+ node: 'dbms_schedule',
+ label: gettext('DBMS Schedules'),
+ type: 'coll-dbms_schedule',
+ columns: ['jsscname', 'jsscrepeatint', 'jsscdesc'],
+ hasSQL: false,
+ hasDepends: false,
+ hasStatistics: false,
+ hasScriptTypes: [],
+ canDrop: true,
+ canDropCascade: false,
+ });
+ }
+
+ if (!pgBrowser.Nodes['dbms_schedule']) {
+ pgAdmin.Browser.Nodes['dbms_schedule'] = pgAdmin.Browser.Node.extend({
+ parent_type: 'dbms_job_scheduler',
+ type: 'dbms_schedule',
+ label: gettext('DBMS Schedule'),
+ node_image: 'icon-pga_schedule',
+ epasHelp: true,
+ epasURL: 'https://www.enterprisedb.com/docs/epas/$VERSION$/epas_compat_bip_guide/03_built-in_packages/15_dbms_scheduler/04_create_schedule/',
+ dialogHelp: url_for('help.static', {'filename': 'dbms_schedule.html'}),
+ canDrop: true,
+ hasSQL: true,
+ hasDepends: false,
+ hasStatistics: false,
+ Init: function() {
+ /* Avoid multiple registration of menus */
+ if (this.initialized)
+ return;
+
+ this.initialized = true;
+
+ pgBrowser.add_menus([{
+ name: 'create_dbms_schedule_on_coll', node: 'coll-dbms_schedule', module: this,
+ applies: ['object', 'context'], callback: 'show_obj_properties',
+ category: 'create', priority: 4, label: gettext('DBMS Schedule...'),
+ data: {action: 'create'},
+ },{
+ name: 'create_dbms_schedule', node: 'dbms_schedule', module: this,
+ applies: ['object', 'context'], callback: 'show_obj_properties',
+ category: 'create', priority: 4, label: gettext('DBMS Schedule...'),
+ data: {action: 'create'},
+ },{
+ name: 'create_dbms_schedule', node: 'dbms_job_scheduler', module: this,
+ applies: ['object', 'context'], callback: 'show_obj_properties',
+ category: 'create', priority: 4, label: gettext('DBMS Schedule...'),
+ data: {action: 'create'},
+ },
+ ]);
+ },
+ getSchema: ()=>new DBMSScheduleSchema(),
+
+ });
+ }
+
+ return pgBrowser.Nodes['dbms_schedule'];
+});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/static/js/dbms_schedule.ui.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/static/js/dbms_schedule.ui.js
new file mode 100644
index 000000000..9872c2525
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/static/js/dbms_schedule.ui.js
@@ -0,0 +1,89 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2024, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from 'sources/gettext';
+import BaseUISchema from 'sources/SchemaView/base_schema.ui';
+import { isEmptyString } from 'sources/validators';
+import moment from 'moment';
+import { getRepeatSchema } from '../../../static/js/dbms_job_scheduler_common.ui';
+
+export default class DBMSScheduleSchema extends BaseUISchema {
+ constructor() {
+ super({
+ jsscid: null,
+ jsscname: '',
+ jsscstart: null,
+ jsscend: null,
+ jsscdesc: '',
+ jsscrepeatint: '',
+ jsscfreq: null,
+ jsscdate: null,
+ jsscweekdays: null,
+ jsscmonthdays: null,
+ jsscmonths: null,
+ jsschours: null,
+ jsscminutes: null,
+ });
+ }
+
+ get idAttribute() {
+ return 'jsscid';
+ }
+
+ get baseFields() {
+ let obj = this;
+ return [
+ {
+ id: 'jsscid', label: gettext('ID'), type: 'int', mode: ['properties'],
+ readonly: function(state) {return !obj.isNew(state); },
+ }, {
+ id: 'jsscname', label: gettext('Name'), cell: 'text',
+ editable: false, type: 'text', noEmpty: true,
+ readonly: function(state) {return !obj.isNew(state); },
+ },
+ // Add the Repeat Schema.
+ ...getRepeatSchema(obj, 'schedule'),
+ {
+ id: 'jsscdesc', label: gettext('Comment'), type: 'multiline',
+ readonly: function(state) {return !obj.isNew(state); },
+ },
+ ];
+ }
+
+ validate(state, setError) {
+ if (isEmptyString(state.jsscstart) && isEmptyString(state.jsscfreq) &&
+ isEmptyString(state.jsscmonths) && isEmptyString(state.jsscweekdays) &&
+ isEmptyString(state.jsscmonthdays) && isEmptyString(state.jsschours) &&
+ isEmptyString(state.jsscminutes) && isEmptyString(state.jsscdate)) {
+ setError('jsscstart', gettext('Either Start time or Repeat interval must be specified.'));
+ return true;
+ } else {
+ setError('jsscstart', null);
+ }
+
+ if (!isEmptyString(state.jsscend)) {
+ let start_time = state.jsscstart,
+ end_time = state.jsscend,
+ start_time_js = start_time.split(' '),
+ end_time_js = end_time.split(' ');
+
+ start_time_js = moment(start_time_js[0] + ' ' + start_time_js[1]);
+ end_time_js = moment(end_time_js[0] + ' ' + end_time_js[1]);
+
+ if(end_time_js.isBefore(start_time_js)) {
+ setError('jsscend', gettext('Start time must be less than end time'));
+ return true;
+ } else {
+ setError('jsscend', null);
+ }
+ } else {
+ state.jsscend = null;
+ }
+ }
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/create.sql
new file mode 100644
index 000000000..aebd77cc4
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/create.sql
@@ -0,0 +1,22 @@
+{% if display_comments %}
+-- DBMS Schedule: '{{ schedule_name }}'
+
+-- EXEC dbms_scheduler.DROP_SCHEDULE('{{ schedule_name }}');
+
+{% endif %}
+EXEC dbms_scheduler.CREATE_SCHEDULE(
+ schedule_name => {{ schedule_name|qtLiteral(conn) }},
+ repeat_interval => {{ repeat_interval|qtLiteral(conn) }}{% if start_date or end_date or comments %},{% endif %}
+{% if start_date %}
+
+ start_date => {{ start_date|qtLiteral(conn) }}{% if end_date or comments %},{% endif %}
+{% endif %}
+{% if end_date %}
+
+ end_date => {{ end_date|qtLiteral(conn) }}{% if comments %},{% endif %}
+{% endif %}
+{% if comments %}
+
+ comments => {{ comments|qtLiteral(conn) }}
+{% endif %}
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/delete.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/delete.sql
new file mode 100644
index 000000000..dae6dbfd9
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/delete.sql
@@ -0,0 +1,6 @@
+EXEC dbms_scheduler.DROP_SCHEDULE(
+ {{ schedule_name|qtLiteral(conn) }}{% if force %},{% endif %}
+{% if force %}
+ {{ force }}
+{% endif %}
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/get_schedule_id.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/get_schedule_id.sql
new file mode 100644
index 000000000..a7794be3a
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/get_schedule_id.sql
@@ -0,0 +1,4 @@
+SELECT
+ dss_schedule_id as jsscid, dss_schedule_name as jsscname
+FROM sys.scheduler_0300_schedule
+WHERE dss_schedule_name={{ jsscname|qtLiteral(conn) }}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/nodes.sql
new file mode 100644
index 000000000..9313c96de
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/nodes.sql
@@ -0,0 +1,5 @@
+SELECT
+ dss_schedule_id as jsscid, dss_schedule_name as jsscname,
+ dss_comments as jsscdesc
+FROM sys.scheduler_0300_schedule sct
+ JOIN sys.dba_scheduler_schedules scv ON sct.dss_schedule_name = scv.schedule_name;
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/properties.sql
new file mode 100644
index 000000000..ca8ea8202
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/properties.sql
@@ -0,0 +1,12 @@
+SELECT
+ dss_schedule_id as jsscid, dss_schedule_name as jsscname,
+ dss_start_date as jsscstart, dss_end_date as jsscend,
+ dss_repeat_interval as jsscrepeatint, dss_comments as jsscdesc
+FROM sys.scheduler_0300_schedule sct
+{% if not jsscid %}
+ JOIN sys.dba_scheduler_schedules scv ON sct.dss_schedule_name = scv.schedule_name
+{% endif %}
+{% if jsscid %}
+WHERE dss_schedule_id={{jsscid}}::oid
+{% endif %}
+ORDER BY dss_schedule_name
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/dbms_schedules_test_data.json b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/dbms_schedules_test_data.json
new file mode 100644
index 000000000..d216ef160
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/dbms_schedules_test_data.json
@@ -0,0 +1,335 @@
+{
+ "dbms_create_schedule": [
+ {
+ "name": "Create schedule: YEARLY",
+ "url": "/browser/dbms_schedule/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsscid": null,
+ "jsscname": "dbms_test_sch_yearly",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2024-02-28 00:00:00 +05:30",
+ "jsscdesc": "This is yearly test schedule",
+ "jsscrepeatint": "",
+ "jsscfreq": "YEARLY",
+ "jsscdate": null,
+ "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"],
+ "jsscmonthdays": ["2", "8", "31", "27"],
+ "jsscmonths": ["1", "5", "12"],
+ "jsschours": ["05", "18", "22"],
+ "jsscminutes": ["45", "37", "58"]
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create schedule: YEARLY BY DATE",
+ "url": "/browser/dbms_schedule/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsscid": null,
+ "jsscname": "dbms_test_sch_yearly_by_date",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2024-02-28 00:00:00 +05:30",
+ "jsscdesc": "This is yearly by date test schedule",
+ "jsscrepeatint": "",
+ "jsscfreq": "YEARLY",
+ "jsscdate": "20250113",
+ "jsscweekdays": [],
+ "jsscmonthdays": [],
+ "jsscmonths": [],
+ "jsschours": [],
+ "jsscminutes": []
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create schedule: MONTHLY",
+ "url": "/browser/dbms_schedule/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsscid": null,
+ "jsscname": "dbms_test_sch_monthly",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2024-02-28 00:00:00 +05:30",
+ "jsscdesc": "This is monthly test schedule",
+ "jsscrepeatint": "",
+ "jsscfreq": "MONTHLY",
+ "jsscdate": null,
+ "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"],
+ "jsscmonthdays": ["2", "8", "31", "27"],
+ "jsscmonths": ["1", "5", "12"],
+ "jsschours": ["05", "18", "22"],
+ "jsscminutes": ["45", "37", "58"]
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create schedule: WEEKLY",
+ "url": "/browser/dbms_schedule/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsscid": null,
+ "jsscname": "dbms_test_sch_weekly",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2024-02-28 00:00:00 +05:30",
+ "jsscdesc": "This is weekly test schedule",
+ "jsscrepeatint": "",
+ "jsscfreq": "WEEKLY",
+ "jsscdate": null,
+ "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"],
+ "jsscmonthdays": ["2", "8", "31", "27"],
+ "jsscmonths": ["1", "5", "12"],
+ "jsschours": ["05", "18", "22"],
+ "jsscminutes": ["45", "37", "58"]
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create schedule: DAILY",
+ "url": "/browser/dbms_schedule/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsscid": null,
+ "jsscname": "dbms_test_sch_daily",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2024-02-28 00:00:00 +05:30",
+ "jsscdesc": "This is daily test schedule",
+ "jsscrepeatint": "",
+ "jsscfreq": "DAILY",
+ "jsscdate": null,
+ "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"],
+ "jsscmonthdays": ["2", "8", "31", "27"],
+ "jsscmonths": ["1", "5", "12"],
+ "jsschours": ["05", "18", "22"],
+ "jsscminutes": ["45", "37", "58"]
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create schedule: HOURLY",
+ "url": "/browser/dbms_schedule/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsscid": null,
+ "jsscname": "dbms_test_sch_hourly",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2024-02-28 00:00:00 +05:30",
+ "jsscdesc": "This is hourly test schedule",
+ "jsscrepeatint": "",
+ "jsscfreq": "HOURLY",
+ "jsscdate": null,
+ "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"],
+ "jsscmonthdays": ["2", "8", "31", "27"],
+ "jsscmonths": ["1", "5", "12"],
+ "jsschours": ["05", "18", "22"],
+ "jsscminutes": ["45", "37", "58"]
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create schedule: MINUTELY",
+ "url": "/browser/dbms_schedule/obj/",
+ "is_positive_test": true,
+ "test_data": {
+ "jsscid": null,
+ "jsscname": "dbms_test_sch_minutely",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2024-02-28 00:00:00 +05:30",
+ "jsscdesc": "This is minutely test schedule",
+ "jsscrepeatint": "",
+ "jsscfreq": "MINUTELY",
+ "jsscdate": null,
+ "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"],
+ "jsscmonthdays": ["2", "8", "31", "27"],
+ "jsscmonths": ["1", "5", "12"],
+ "jsschours": ["05", "18", "22"],
+ "jsscminutes": ["45", "37", "58"]
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ }
+ },
+ {
+ "name": "Create schedule: while server is down",
+ "url": "/browser/dbms_schedule/obj/",
+ "is_positive_test": false,
+ "test_data": {
+ "jsscid": null,
+ "jsscname": "dbms_test_sch_yearly",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2024-02-28 00:00:00 +05:30",
+ "jsscdesc": "This is yearly test schedule",
+ "jsscrepeatint": "",
+ "jsscfreq": "YEARLY",
+ "jsscdate": null,
+ "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"],
+ "jsscmonthdays": ["2", "8", "31", "27"],
+ "jsscmonths": ["1", "5", "12"],
+ "jsschours": ["05", "18", "22"],
+ "jsscminutes": ["45", "37", "58"]
+ },
+ "mocking_required": true,
+ "mock_data": {
+ "function_name": "pgadmin.utils.driver.psycopg3.connection.Connection.execute_scalar",
+ "return_value": "[(False,'Mocked Internal Server Error')]"
+ },
+ "expected_data": {
+ "status_code": 500,
+ "error_msg": "Mocked Internal Server Error",
+ "test_result_data": {}
+ }
+ }
+ ],
+ "dbms_delete_schedule": [
+ {
+ "name": "Delete schedule: With existing DBMS schedule.",
+ "url": "/browser/dbms_schedule/obj/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": false
+ },
+ {
+ "name": "Delete multiple schedules: With existing DBMS schedule.",
+ "url": "/browser/dbms_schedule/obj/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": true
+ }
+ ],
+ "dbms_get_schedule": [
+ {
+ "name": "Get schedule: With existing DBMS schedule.",
+ "url": "/browser/dbms_schedule/obj/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": false
+ },
+ {
+ "name": "Get schedules: With multiple existing DBMS schedules.",
+ "url": "/browser/dbms_schedule/obj/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": true
+ },
+ {
+ "name": "Get schedule: while server down.",
+ "url": "/browser/dbms_schedule/obj/",
+ "is_positive_test": false,
+ "inventory_data": {},
+ "test_data": {},
+ "mocking_required": true,
+ "mock_data": {
+ "function_name": "pgadmin.utils.driver.psycopg3.connection.Connection.execute_dict",
+ "return_value": "(False,'Mocked Internal Server Error')"
+ },
+ "expected_data": {
+ "status_code": 500,
+ "error_msg": "Mocked Internal Server Error",
+ "test_result_data": {}
+ },
+ "is_list": false
+ }
+ ],
+ "dbms_msql_schedule": [
+ {
+ "name": "Get schedule msql: For existing dbms schedule.",
+ "url": "/browser/dbms_schedule/msql/",
+ "is_positive_test": true,
+ "inventory_data": {},
+ "test_data": {
+ "jsscname": "dbms_test_sch_msql",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2024-02-28 00:00:00 +05:30",
+ "jsscdesc": "This is daily test schedule",
+ "jsscrepeatint": "",
+ "jsscfreq": "DAILY",
+ "jsscdate": null,
+ "jsscweekdays": [],
+ "jsscmonthdays": [],
+ "jsscmonths": [],
+ "jsschours": [],
+ "jsscminutes": []
+ },
+ "mocking_required": false,
+ "mock_data": {},
+ "expected_data": {
+ "status_code": 200,
+ "error_msg": null,
+ "test_result_data": {}
+ },
+ "is_list": false
+ }
+ ]
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_all.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_all.sql
new file mode 100644
index 000000000..43c420216
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_all.sql
@@ -0,0 +1,11 @@
+-- DBMS Schedule: 'dbms_test_sch_yearly'
+
+-- EXEC dbms_scheduler.DROP_SCHEDULE('dbms_test_sch_yearly');
+
+EXEC dbms_scheduler.CREATE_SCHEDULE(
+ schedule_name => 'dbms_test_sch_yearly',
+ repeat_interval => 'FREQ=YEARLY;BYMONTH=JAN,MAY,DEC;BYMONTHDAY=2,8,31,27;BYDAY=SUN,MON,TUE,WED,THU,FRI,SAT;BYHOUR=05,18,22;BYMINUTE=45,37,58',
+ start_date => '2024-02-27 00:00:00+05:30',
+ end_date => '2024-02-28 00:00:00+05:30',
+ comments => 'This is yearly test schedule'
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_all_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_all_msql.sql
new file mode 100644
index 000000000..8c615d920
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_all_msql.sql
@@ -0,0 +1,7 @@
+EXEC dbms_scheduler.CREATE_SCHEDULE(
+ schedule_name => 'dbms_test_sch_yearly',
+ repeat_interval => 'FREQ=YEARLY;BYMONTH=JAN,MAY,DEC;BYMONTHDAY=2,8,31,27;BYDAY=SUN,MON,TUE,WED,THU,FRI,SAT;BYHOUR=05,18,22;BYMINUTE=45,37,58',
+ start_date => '2024-02-27 00:00:00 +05:30',
+ end_date => '2024-02-28 00:00:00 +05:30',
+ comments => 'This is yearly test schedule'
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_bydate.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_bydate.sql
new file mode 100644
index 000000000..7c7d7eaa2
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_bydate.sql
@@ -0,0 +1,11 @@
+-- DBMS Schedule: 'dbms_test_sch_yearly_by_date'
+
+-- EXEC dbms_scheduler.DROP_SCHEDULE('dbms_test_sch_yearly_by_date');
+
+EXEC dbms_scheduler.CREATE_SCHEDULE(
+ schedule_name => 'dbms_test_sch_yearly_by_date',
+ repeat_interval => 'FREQ=YEARLY;BYDATE=20250113;',
+ start_date => '2024-02-27 00:00:00+05:30',
+ end_date => '2024-02-28 00:00:00+05:30',
+ comments => 'This is yearly by date test schedule'
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_bydate_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_bydate_msql.sql
new file mode 100644
index 000000000..332db2451
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_bydate_msql.sql
@@ -0,0 +1,7 @@
+EXEC dbms_scheduler.CREATE_SCHEDULE(
+ schedule_name => 'dbms_test_sch_yearly_by_date',
+ repeat_interval => 'FREQ=YEARLY;BYDATE=20250113;',
+ start_date => '2024-02-27 00:00:00 +05:30',
+ end_date => '2024-02-28 00:00:00 +05:30',
+ comments => 'This is yearly by date test schedule'
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq.sql
new file mode 100644
index 000000000..029edffaa
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq.sql
@@ -0,0 +1,7 @@
+-- DBMS Schedule: 'dbms_test_sch_daily_freq'
+
+-- EXEC dbms_scheduler.DROP_SCHEDULE('dbms_test_sch_daily_freq');
+
+EXEC dbms_scheduler.CREATE_SCHEDULE(
+ schedule_name => 'dbms_test_sch_daily_freq',
+ repeat_interval => 'FREQ=DAILY;');
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_comm.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_comm.sql
new file mode 100644
index 000000000..b6f2c1415
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_comm.sql
@@ -0,0 +1,9 @@
+-- DBMS Schedule: 'dbms_test_sch_weekly_comm'
+
+-- EXEC dbms_scheduler.DROP_SCHEDULE('dbms_test_sch_weekly_comm');
+
+EXEC dbms_scheduler.CREATE_SCHEDULE(
+ schedule_name => 'dbms_test_sch_weekly_comm',
+ repeat_interval => 'FREQ=WEEKLY;',
+ comments => 'This is weekly test schedule'
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_comm_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_comm_msql.sql
new file mode 100644
index 000000000..69c03dc7d
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_comm_msql.sql
@@ -0,0 +1,5 @@
+EXEC dbms_scheduler.CREATE_SCHEDULE(
+ schedule_name => 'dbms_test_sch_weekly_comm',
+ repeat_interval => 'FREQ=WEEKLY;',
+ comments => 'This is weekly test schedule'
+);
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_msql.sql
new file mode 100644
index 000000000..e6d25e32d
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_msql.sql
@@ -0,0 +1,3 @@
+EXEC dbms_scheduler.CREATE_SCHEDULE(
+ schedule_name => 'dbms_test_sch_daily_freq',
+ repeat_interval => 'FREQ=DAILY;');
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_start_date.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_start_date.sql
new file mode 100644
index 000000000..f989e273a
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_start_date.sql
@@ -0,0 +1,8 @@
+-- DBMS Schedule: 'dbms_test_sch_monthly_start_date'
+
+-- EXEC dbms_scheduler.DROP_SCHEDULE('dbms_test_sch_monthly_start_date');
+
+EXEC dbms_scheduler.CREATE_SCHEDULE(
+ schedule_name => 'dbms_test_sch_monthly_start_date',
+ repeat_interval => 'FREQ=MONTHLY;',
+ start_date => '2024-02-27 00:00:00+05:30');
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_start_date_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_start_date_msql.sql
new file mode 100644
index 000000000..d115325f8
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_start_date_msql.sql
@@ -0,0 +1,4 @@
+EXEC dbms_scheduler.CREATE_SCHEDULE(
+ schedule_name => 'dbms_test_sch_monthly_start_date',
+ repeat_interval => 'FREQ=MONTHLY;',
+ start_date => '2024-02-27 00:00:00 +05:30');
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/test.json b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/test.json
new file mode 100644
index 000000000..37f861348
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/test.json
@@ -0,0 +1,188 @@
+{
+ "scenarios": [
+ {
+ "type": "create",
+ "name": "Create extension 'edb_job_scheduler' for DBMS Schedule",
+ "endpoint": "NODE-extension.obj",
+ "sql_endpoint": "NODE-extension.sql_id",
+ "data": {
+ "name": "edb_job_scheduler"
+ },
+ "store_object_id": true
+ },
+ {
+ "type": "create",
+ "name": "Create extension 'dbms_scheduler' for DBMS Schedule",
+ "endpoint": "NODE-extension.obj",
+ "sql_endpoint": "NODE-extension.sql_id",
+ "data": {
+ "name": "dbms_scheduler"
+ },
+ "store_object_id": true
+ },
+ {
+ "type": "create",
+ "name": "Create Schedule with all options",
+ "endpoint": "NODE-dbms_schedule.obj",
+ "sql_endpoint": "NODE-dbms_schedule.sql_id",
+ "msql_endpoint": "NODE-dbms_schedule.msql",
+ "data": {
+ "jsscname": "dbms_test_sch_yearly",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2024-02-28 00:00:00 +05:30",
+ "jsscdesc": "This is yearly test schedule",
+ "jsscrepeatint": "",
+ "jsscfreq": "YEARLY",
+ "jsscdate": null,
+ "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"],
+ "jsscmonthdays": ["2", "8", "31", "27"],
+ "jsscmonths": ["1", "5", "12"],
+ "jsschours": ["05", "18", "22"],
+ "jsscminutes": ["45", "37", "58"]
+ },
+ "expected_sql_file": "create_schedule_all.sql",
+ "expected_msql_file": "create_schedule_all_msql.sql"
+ },
+ {
+ "type": "delete",
+ "name": "Drop Schedule",
+ "endpoint": "NODE-dbms_schedule.obj_id",
+ "data": {
+ "name": "dbms_test_sch_yearly"
+ }
+ },
+ {
+ "type": "create",
+ "name": "Create Schedule Yearly by date",
+ "endpoint": "NODE-dbms_schedule.obj",
+ "sql_endpoint": "NODE-dbms_schedule.sql_id",
+ "msql_endpoint": "NODE-dbms_schedule.msql",
+ "data": {
+ "jsscname": "dbms_test_sch_yearly_by_date",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "2024-02-28 00:00:00 +05:30",
+ "jsscdesc": "This is yearly by date test schedule",
+ "jsscrepeatint": "",
+ "jsscfreq": "YEARLY",
+ "jsscdate": "20250113",
+ "jsscweekdays": [],
+ "jsscmonthdays": [],
+ "jsscmonths": [],
+ "jsschours": [],
+ "jsscminutes": []
+ },
+ "expected_sql_file": "create_schedule_bydate.sql",
+ "expected_msql_file": "create_schedule_bydate_msql.sql"
+ },
+ {
+ "type": "delete",
+ "name": "Drop Schedule",
+ "endpoint": "NODE-dbms_schedule.obj_id",
+ "data": {
+ "name": "dbms_test_sch_yearly_by_date"
+ }
+ },
+ {
+ "type": "create",
+ "name": "Create Schedule only start date",
+ "endpoint": "NODE-dbms_schedule.obj",
+ "sql_endpoint": "NODE-dbms_schedule.sql_id",
+ "msql_endpoint": "NODE-dbms_schedule.msql",
+ "data": {
+ "jsscname": "dbms_test_sch_monthly_start_date",
+ "jsscstart": "2024-02-27 00:00:00 +05:30",
+ "jsscend": "",
+ "jsscdesc": "",
+ "jsscrepeatint": "",
+ "jsscfreq": "MONTHLY",
+ "jsscdate": null,
+ "jsscweekdays": [],
+ "jsscmonthdays": [],
+ "jsscmonths": [],
+ "jsschours": [],
+ "jsscminutes": []
+ },
+ "expected_sql_file": "create_schedule_start_date.sql",
+ "expected_msql_file": "create_schedule_start_date_msql.sql"
+ },
+ {
+ "type": "delete",
+ "name": "Drop Schedule",
+ "endpoint": "NODE-dbms_schedule.obj_id",
+ "data": {
+ "name": "dbms_test_sch_monthly_start_date"
+ }
+ },
+ {
+ "type": "create",
+ "name": "Create Schedule only frequency",
+ "endpoint": "NODE-dbms_schedule.obj",
+ "sql_endpoint": "NODE-dbms_schedule.sql_id",
+ "msql_endpoint": "NODE-dbms_schedule.msql",
+ "data": {
+ "jsscname": "dbms_test_sch_daily_freq",
+ "jsscstart": "",
+ "jsscend": "",
+ "jsscdesc": "",
+ "jsscrepeatint": "",
+ "jsscfreq": "DAILY",
+ "jsscdate": null,
+ "jsscweekdays": [],
+ "jsscmonthdays": [],
+ "jsscmonths": [],
+ "jsschours": [],
+ "jsscminutes": []
+ },
+ "expected_sql_file": "create_schedule_freq.sql",
+ "expected_msql_file": "create_schedule_freq_msql.sql"
+ },
+ {
+ "type": "delete",
+ "name": "Drop Schedule",
+ "endpoint": "NODE-dbms_schedule.obj_id",
+ "data": {
+ "name": "dbms_test_sch_daily_freq"
+ }
+ },
+ {
+ "type": "create",
+ "name": "Create Schedule frequency with comment",
+ "endpoint": "NODE-dbms_schedule.obj",
+ "sql_endpoint": "NODE-dbms_schedule.sql_id",
+ "msql_endpoint": "NODE-dbms_schedule.msql",
+ "data": {
+ "jsscname": "dbms_test_sch_weekly_comm",
+ "jsscstart": "",
+ "jsscend": "",
+ "jsscdesc": "This is weekly test schedule",
+ "jsscrepeatint": "",
+ "jsscfreq": "WEEKLY",
+ "jsscdate": null,
+ "jsscweekdays": [],
+ "jsscmonthdays": [],
+ "jsscmonths": [],
+ "jsschours": [],
+ "jsscminutes": []
+ },
+ "expected_sql_file": "create_schedule_freq_comm.sql",
+ "expected_msql_file": "create_schedule_freq_comm_msql.sql"
+ },
+ {
+ "type": "delete",
+ "name": "Drop Schedule",
+ "endpoint": "NODE-dbms_schedule.obj_id",
+ "data": {
+ "name": "dbms_test_sch_weekly_comm"
+ }
+ },
+ {
+ "type": "delete",
+ "name": "Drop Extension",
+ "endpoint": "NODE-extension.delete",
+ "data": {
+ "ids": [""]
+ },
+ "preprocess_data": true
+ }
+ ]
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_add_schedule.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_add_schedule.py
new file mode 100644
index 000000000..c08e468c4
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_add_schedule.py
@@ -0,0 +1,83 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import os
+import json
+from unittest.mock import patch
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_schedules_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSAddScheduleTestCase(BaseTestGenerator):
+ """This class will test the add schedule in the DBMS Schedule API"""
+ scenarios = utils.generate_scenarios("dbms_create_schedule",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ def runTest(self):
+ """ This function will add DBMS Schedule under test database. """
+ if self.is_positive_test:
+ response = job_scheduler_utils.api_create(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ # Verify in backend
+ response_data = json.loads(response.data)
+ self.schedule_id = response_data['node']['_id']
+ schedule_name = response_data['node']['label']
+ is_present = job_scheduler_utils.verify_dbms_schedule(
+ self, schedule_name)
+ self.assertTrue(
+ is_present,"DBMS schedule was not created successfully.")
+ else:
+ if self.mocking_required:
+ with patch(self.mock_data["function_name"],
+ side_effect=eval(self.mock_data["return_value"])):
+ response = job_scheduler_utils.api_create(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+ utils.assert_error_message(self, response)
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_delete_schedule.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_delete_schedule.py
new file mode 100644
index 000000000..77bd31851
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_delete_schedule.py
@@ -0,0 +1,98 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import uuid
+import os
+import json
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_schedules_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSDeleteScheduleTestCase(BaseTestGenerator):
+ """This class will test the add schedule in the DBMS Schedule API"""
+ scenarios = utils.generate_scenarios("dbms_delete_schedule",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ self.sch_name = "test_schedule_delete%s" % str(uuid.uuid4())[1:8]
+ self.schedule_id = job_scheduler_utils.create_dbms_schedule(
+ self, self.sch_name)
+
+ # multiple schedules
+ if self.is_list:
+ self.sch_name2 = "test_schedule_delete%s" % str(uuid.uuid4())[1:8]
+ self.schedule_id_2 = job_scheduler_utils.create_dbms_schedule(
+ self, self.sch_name2)
+
+ def runTest(self):
+ """
+ This function will test delete DBMS Schedule under test database.
+ """
+ if self.is_list:
+ self.data['ids'] = [self.schedule_id, self.schedule_id_2]
+ response = job_scheduler_utils.api_delete(self, '')
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ is_present = job_scheduler_utils.verify_dbms_schedule(
+ self, self.sch_name)
+ self.assertFalse(
+ is_present, "DBMS schedule was not deleted successfully")
+
+ is_present = job_scheduler_utils.verify_dbms_schedule(
+ self, self.sch_name2)
+ self.assertFalse(
+ is_present, "DBMS schedule was not deleted successfully")
+ else:
+ response = job_scheduler_utils.api_delete(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ is_present = job_scheduler_utils.verify_dbms_schedule(
+ self, self.sch_name)
+ self.assertFalse(
+ is_present, "DBMS schedule was not deleted successfully")
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_get_msql_schedule.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_get_msql_schedule.py
new file mode 100644
index 000000000..c3cd8db83
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_get_msql_schedule.py
@@ -0,0 +1,65 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import os
+import json
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_schedules_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSGetMSQLScheduleTestCase(BaseTestGenerator):
+ """This class will test the add schedule in the DBMS Schedule API"""
+ scenarios = utils.generate_scenarios("dbms_msql_schedule",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ def runTest(self):
+ """ This function will add DBMS Schedule under test database. """
+ url_encode_data = self.data
+
+ response = job_scheduler_utils.api_get_msql(self, url_encode_data)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_get_schedule.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_get_schedule.py
new file mode 100644
index 000000000..9a996e58e
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_get_schedule.py
@@ -0,0 +1,92 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import uuid
+import os
+import json
+from unittest.mock import patch
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from ...tests import utils as job_scheduler_utils
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+
+# Load test data from json file.
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/dbms_schedules_test_data.json") as data_file:
+ test_cases = json.load(data_file)
+
+
+class DBMSGetScheduleTestCase(BaseTestGenerator):
+ """This class will test the add schedule in the DBMS Schedule API"""
+ scenarios = utils.generate_scenarios("dbms_get_schedule",
+ test_cases)
+
+ def setUp(self):
+ super().setUp()
+ # Load test data
+ self.data = self.test_data
+
+ if not job_scheduler_utils.is_supported_version(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG)
+
+ # Create db
+ self.db_name, self.db_id = job_scheduler_utils.create_test_database(
+ self)
+ db_con = database_utils.connect_database(self,
+ utils.SERVER_GROUP,
+ self.server_id,
+ self.db_id)
+ if db_con["info"] != "Database connected.":
+ raise Exception("Could not connect to database.")
+
+ # Create extension required for job scheduler
+ job_scheduler_utils.create_job_scheduler_extensions(self)
+
+ if not job_scheduler_utils.is_dbms_job_scheduler_present(self):
+ self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION)
+
+ self.sch_name = "test_schedule_get%s" % str(uuid.uuid4())[1:8]
+ self.schedule_id = job_scheduler_utils.create_dbms_schedule(
+ self, self.sch_name)
+
+ # multiple schedules
+ if self.is_list:
+ self.sch_name2 = "test_schedule_get%s" % str(uuid.uuid4())[1:8]
+ self.schedule_id_2 = job_scheduler_utils.create_dbms_schedule(
+ self, self.sch_name2)
+
+ def runTest(self):
+ """ This function will test DBMS Schedule under test database."""
+ if self.is_positive_test:
+ if self.is_list:
+ response = job_scheduler_utils.api_get(self, '')
+ else:
+ response = job_scheduler_utils.api_get(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+ else:
+ if self.mocking_required:
+ with patch(self.mock_data["function_name"],
+ side_effect=[eval(self.mock_data["return_value"])]):
+ response = job_scheduler_utils.api_get(self)
+
+ # Assert response
+ utils.assert_status_code(self, response)
+ utils.assert_error_message(self, response)
+
+ def tearDown(self):
+ """This function will do the cleanup task."""
+ job_scheduler_utils.delete_dbms_schedule(self, self.sch_name)
+ if self.is_list:
+ job_scheduler_utils.delete_dbms_schedule(self, self.sch_name2)
+
+ job_scheduler_utils.clean_up(self)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/img/coll-dbms_job_scheduler.svg b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/img/coll-dbms_job_scheduler.svg
new file mode 100644
index 000000000..848254661
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/img/coll-dbms_job_scheduler.svg
@@ -0,0 +1,16 @@
+
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_job_scheduler.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_job_scheduler.js
new file mode 100644
index 000000000..db52b9173
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_job_scheduler.js
@@ -0,0 +1,47 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2024, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import DBMSJobSchedulerSchema from './dbms_jobscheduler.ui';
+
+define('pgadmin.node.dbms_job_scheduler', [
+ 'sources/gettext', 'sources/pgadmin',
+ 'pgadmin.browser', 'pgadmin.browser.collection', 'pgadmin.node.dbms_job',
+ 'pgadmin.node.dbms_program', 'pgadmin.node.dbms_schedule',
+], function(gettext, pgAdmin, pgBrowser) {
+
+
+ if (!pgBrowser.Nodes['dbms_job_scheduler']) {
+ pgAdmin.Browser.Nodes['dbms_job_scheduler'] = pgAdmin.Browser.Node.extend({
+ parent_type: 'database',
+ type: 'dbms_job_scheduler',
+ label: gettext('DBMS Job Scheduler'),
+ columns: ['jobname', 'jobstatus', 'joberror', 'jobstarttime', 'jobendtime', 'jobnextrun'],
+ canDrop: false,
+ canDropCascade: false,
+ canSelect: false,
+ hasSQL: false,
+ hasDepends: false,
+ hasStatistics: false,
+ hasScriptTypes: [],
+ Init: function() {
+ /* Avoid multiple registration of menus */
+ if (this.initialized)
+ return;
+
+ this.initialized = true;
+
+ },
+ getSchema: ()=> {
+ return new DBMSJobSchedulerSchema();
+ }
+ });
+ }
+
+ return pgBrowser.Nodes['dbms_job_scheduler'];
+});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_job_scheduler_common.ui.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_job_scheduler_common.ui.js
new file mode 100644
index 000000000..192ccb697
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_job_scheduler_common.ui.js
@@ -0,0 +1,255 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2024, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from 'sources/gettext';
+import BaseUISchema from 'sources/SchemaView/base_schema.ui';
+import { WEEKDAYS, MONTHDAYS, MONTHS, HOURS, MINUTES } from '../../../../../../static/js/constants';
+
+function isReadOnly(obj, state, parentName) {
+ // Always true in case of edit dialog
+ if (!obj.isNew(state))
+ return true;
+
+ // Check the parent name and based on the condition return appropriate value.
+ if (parentName == 'job') {
+ return obj.isNew(state) && state.jsjobtype == 'p';
+ }
+ return false;
+}
+
+
+export function getRepeatSchema(obj, parentName) {
+ return [
+ {
+ id: 'jsscrepeatint', label: gettext('Repeat Interval'), cell: 'text',
+ readonly: true, type: 'multiline', mode: ['edit', 'properties'],
+ group: gettext('Repeat'),
+ }, {
+ id: 'jsscstart', label: gettext('Start'), type: 'datetimepicker',
+ cell: 'datetimepicker', group: gettext('Repeat'),
+ controlProps: { ampm: false,
+ placeholder: gettext('YYYY-MM-DD HH:mm:ss Z'), autoOk: true,
+ disablePast: true,
+ },
+ readonly: function(state) { return isReadOnly(obj, state, parentName); },
+ deps:['jsjobtype'],
+ depChange: (state) => {
+ if (state.jsjobtype == 'p') {
+ return { jsscstart: null };
+ }
+ }
+ }, {
+ id: 'jsscend', label: gettext('End'), type: 'datetimepicker',
+ cell: 'datetimepicker', group: gettext('Repeat'),
+ controlProps: { ampm: false,
+ placeholder: gettext('YYYY-MM-DD HH:mm:ss Z'), autoOk: true,
+ disablePast: true,
+ },
+ readonly: function(state) { return isReadOnly(obj, state, parentName); },
+ deps:['jsjobtype'],
+ depChange: (state) => {
+ if (state.jsjobtype == 'p') {
+ return { jsscend: null };
+ }
+ }
+ }, {
+ id: 'jsscfreq', label: gettext('Frequency'), type: 'select',
+ group: gettext('Repeat'),
+ controlProps: { allowClear: true,
+ placeholder: gettext('Select the frequency...'),
+ },
+ options: [
+ {'label': 'YEARLY', 'value': 'YEARLY'},
+ {'label': 'MONTHLY', 'value': 'MONTHLY'},
+ {'label': 'WEEKLY', 'value': 'WEEKLY'},
+ {'label': 'DAILY', 'value': 'DAILY'},
+ {'label': 'HOURLY', 'value': 'HOURLY'},
+ {'label': 'MINUTELY', 'value': 'MINUTELY'},
+ ],
+ readonly: function(state) { return isReadOnly(obj, state, parentName); },
+ deps:['jsjobtype'],
+ depChange: (state) => {
+ if (state.jsjobtype == 'p') {
+ return { jsscfreq: null };
+ }
+ }
+ }, {
+ id: 'jsscdate', label: gettext('Date'), type: 'datetimepicker',
+ cell: 'datetimepicker', group: gettext('Repeat'),
+ controlProps: { ampm: false,
+ placeholder: gettext('YYYYMMDD'), autoOk: true,
+ disablePast: true, pickerType: 'Date', format: 'yyyyMMdd',
+ },
+ readonly: function(state) { return isReadOnly(obj, state, parentName); },
+ deps:['jsjobtype'],
+ depChange: (state) => {
+ if (state.jsjobtype == 'p') {
+ return { jsscdate: null };
+ }
+ }
+ }, {
+ id: 'jsscmonths', label: gettext('Months'), type: 'select',
+ group: gettext('Repeat'),
+ controlProps: { allowClear: true, multiple: true, allowSelectAll: true,
+ placeholder: gettext('Select the months...'),
+ },
+ options: MONTHS,
+ readonly: function(state) { return isReadOnly(obj, state, parentName); },
+ deps:['jsjobtype'],
+ depChange: (state) => {
+ if (state.jsjobtype == 'p') {
+ return { jsscmonths: null };
+ }
+ }
+ }, {
+ id: 'jsscweekdays', label: gettext('Week Days'), type: 'select',
+ group: gettext('Repeat'),
+ controlProps: { allowClear: true, multiple: true, allowSelectAll: true,
+ placeholder: gettext('Select the weekdays...'),
+ },
+ options: WEEKDAYS,
+ readonly: function(state) { return isReadOnly(obj, state, parentName); },
+ deps:['jsjobtype'],
+ depChange: (state) => {
+ if (state.jsjobtype == 'p') {
+ return { jsscweekdays: null };
+ }
+ }
+ }, {
+ id: 'jsscmonthdays', label: gettext('Month Days'), type: 'select',
+ group: gettext('Repeat'),
+ controlProps: { allowClear: true, multiple: true, allowSelectAll: true,
+ placeholder: gettext('Select the month days...'),
+ },
+ options: MONTHDAYS,
+ readonly: function(state) { return isReadOnly(obj, state, parentName); },
+ deps:['jsjobtype'],
+ depChange: (state) => {
+ if (state.jsjobtype == 'p') {
+ return { jsscmonthdays: null };
+ }
+ }
+ }, {
+ id: 'jsschours', label: gettext('Hours'), type: 'select',
+ group: gettext('Repeat'),
+ controlProps: { allowClear: true, multiple: true, allowSelectAll: true,
+ placeholder: gettext('Select the hours...'),
+ },
+ options: HOURS,
+ readonly: function(state) { return isReadOnly(obj, state, parentName); },
+ deps:['jsjobtype'],
+ depChange: (state) => {
+ if (state.jsjobtype == 'p') {
+ return { jsschours: null };
+ }
+ }
+ }, {
+ id: 'jsscminutes', label: gettext('Minutes'), type: 'select',
+ group: gettext('Repeat'),
+ controlProps: { allowClear: true, multiple: true, allowSelectAll: true,
+ placeholder: gettext('Select the minutes...'),
+ },
+ options: MINUTES,
+ readonly: function(state) { return isReadOnly(obj, state, parentName); },
+ deps:['jsjobtype'],
+ depChange: (state) => {
+ if (state.jsjobtype == 'p') {
+ return { jsscminutes: null };
+ }
+ }
+ }
+ ];
+}
+
+export function getActionSchema(obj, parentName) {
+ return [
+ {
+ id: 'jsprtype', label: gettext('Type'), type: 'select',
+ controlProps: { allowClear: false}, group: gettext('Action'),
+ options: [
+ {'label': 'PLSQL BLOCK', 'value': 'PLSQL_BLOCK'},
+ {'label': 'STORED PROCEDURE', 'value': 'STORED_PROCEDURE'},
+ ],
+ readonly: function(state) { return isReadOnly(obj, state, parentName); },
+ deps:['jsjobtype'],
+ depChange: (state) => {
+ if (state.jsjobtype == 'p') {
+ return { jsprtype: null };
+ }
+ }
+ }, {
+ id: 'jsprcode', label: gettext('Code'), type: 'sql',
+ group: gettext('Code'), isFullTab: true,
+ readonly: function(state) {
+ return isReadOnly(obj, state, parentName) || state.jsprtype == 'STORED_PROCEDURE';
+ },
+ deps:['jsprtype', 'jsjobtype'],
+ depChange: (state) => {
+ if (obj.isNew(state) && (state.jsprtype == 'STORED_PROCEDURE' || state.jsjobtype == 'p')) {
+ return { jsprcode: '' };
+ }
+ }
+ }, {
+ id: 'jsprproc', label: gettext('Procedure'), type: 'select',
+ controlProps: { allowClear: false}, group: gettext('Action'),
+ options: obj.fieldOptions.procedures,
+ optionsLoaded: (options) => { obj.jsprocData = options; },
+ readonly: function(state) {
+ return isReadOnly(obj, state, parentName) || state.jsprtype == 'PLSQL_BLOCK';
+ },
+ deps:['jsprtype', 'jsjobtype'],
+ depChange: (state) => {
+ if (obj.isNew(state) && (state.jsprtype == 'PLSQL_BLOCK' || state.jsjobtype == 'p')) {
+ return { jsprproc: null, jsprnoofargs : 0, jsprarguments: [] };
+ }
+
+ for(const option of obj.jsprocData) {
+ if (option.label == state.jsprproc) {
+ return { jsprnoofargs : option.no_of_args,
+ jsprarguments: option.arguments};
+ }
+ }
+ }
+ }, {
+ id: 'jsprnoofargs', label: gettext('Number of Arguments'),
+ type: 'int', group: gettext('Action'), deps:['jsprtype'],
+ readonly: true,
+ }, {
+ id: 'jsprarguments', label: gettext('Arguments'), cell: 'string',
+ group: gettext('Arguments'), type: 'collection',
+ canAdd: false, canDelete: false, canDeleteRow: false, canEdit: false,
+ mode: ['create', 'edit'],
+ columns: parentName == 'job' ? ['argname', 'argtype', 'argdefval', 'argval'] : ['argname', 'argtype', 'argdefval'],
+ schema : new ProgramArgumentSchema(parentName),
+ }
+ ];
+}
+
+export class ProgramArgumentSchema extends BaseUISchema {
+ constructor(parentName) {
+ super();
+ this.parentName = parentName;
+ }
+
+ get baseFields() {
+ return[{
+ id: 'argname', label: gettext('Name'), type: 'text',
+ cell: '', readonly: true,
+ }, {
+ id: 'argtype', label: gettext('Data type'), type: 'text',
+ cell: '', readonly: true,
+ }, {
+ id: 'argdefval', label: gettext('Default'), type: 'text',
+ cell: '', readonly: true,
+ }, {
+ id: 'argval', label: gettext('Value'), type: 'text',
+ cell: 'text',
+ }];
+ }
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_jobscheduler.ui.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_jobscheduler.ui.js
new file mode 100644
index 000000000..136918640
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_jobscheduler.ui.js
@@ -0,0 +1,45 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2024, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from 'sources/gettext';
+import BaseUISchema from 'sources/SchemaView/base_schema.ui';
+
+export default class DBMSJobSchedulerSchema extends BaseUISchema {
+ constructor() {
+ super({
+ jobid: null,
+ jobname: '',
+ jobstatus: '',
+ joberror: ''
+ });
+ }
+
+ get idAttribute() {
+ return 'jobid';
+ }
+
+ get baseFields() {
+ return [
+ {
+ id: 'jobid', label: gettext('ID'), cell: 'int', mode: ['properties']
+ }, {
+ id: 'jobname', label: gettext('Name'), cell: 'text'
+ }, {
+ id: 'jobstatus', label: gettext('Status'), cell: 'text'
+ }, {
+ id: 'joberror', label: gettext('Error'), cell: 'text'
+ }, {
+ id: 'jobstarttime', label: gettext('Start Time'), cell: 'datetimepicker'
+ }, {
+ id: 'jobendtime', label: gettext('End Time'), cell: 'datetimepicker'
+ }, {
+ id: 'jobnextrun', label: gettext('Next Run'), cell: 'datetimepicker'
+ }];
+ }
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/templates/dbms_job_scheduler/ppas/16_plus/get_job_run_details.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/templates/dbms_job_scheduler/ppas/16_plus/get_job_run_details.sql
new file mode 100644
index 000000000..ba55dfe24
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/templates/dbms_job_scheduler/ppas/16_plus/get_job_run_details.sql
@@ -0,0 +1,13 @@
+SELECT
+ scjobs.dsj_job_id as jobid, scjobs.dsj_job_name as jobname,
+ jrd.workerpid, jrd.error as joberror, job.jobnextrun as jobnextrun,
+ jrd.starttime as jobstarttime, jrd.endtime as jobendtime,
+ CASE
+ WHEN jrd.status = 's' THEN 'Success'
+ WHEN jrd.status = 'f' THEN 'Failed'
+ WHEN jrd.status = 'r' THEN 'Running'
+ END as jobstatus
+FROM sys.scheduler_0400_job scjobs
+ JOIN sys.job_run_details jrd ON scjobs.dsj_job_id = jrd.jobid
+ LEFT JOIN sys.jobs job ON scjobs.dsj_job_id = job.jobid
+ORDER BY jobid;
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/tests/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/tests/utils.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/tests/utils.py
new file mode 100644
index 000000000..43915bb57
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/tests/utils.py
@@ -0,0 +1,485 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+import sys
+import json
+import traceback
+from urllib.parse import urlencode
+from regression.python_test_utils import test_utils as utils
+from pgadmin.utils.constants import DBMS_JOB_SCHEDULER_ID
+from pgadmin.browser.server_groups.servers.databases.tests import \
+ utils as database_utils
+
+CONTENT_TYPE = 'html/json'
+FORMAT_STRING = '{0}{1}/{2}/{3}/{4}/{5}'
+TEST_DATABASE = "test_dbms_job_scheduler"
+SKIP_MSG = ("The DBMS Job Scheduler is supported exclusively on EPAS servers "
+ "version 16 or higher.")
+SKIP_MSG_EXTENSION = ("The DBMS Job Scheduler requires the presence of "
+ "'edb_job_scheduler' and 'dbms_scheduler' extensions in "
+ "the test database and that database must be listed in "
+ "the 'edb_job_scheduler.database_list' GUC parameter")
+
+
+# api methods
+def api_create(self):
+ return self.tester.post('{0}{1}/{2}/{3}/{4}/'.
+ format(self.url, utils.SERVER_GROUP,
+ self.server_id, self.db_id,
+ DBMS_JOB_SCHEDULER_ID),
+ data=json.dumps(self.data),
+ content_type=CONTENT_TYPE)
+
+
+def api_delete(self, delete_id=None):
+ if delete_id is None and hasattr(self, 'schedule_id'):
+ delete_id = self.schedule_id
+ elif delete_id is None and hasattr(self, 'program_id'):
+ delete_id = self.program_id
+ elif delete_id is None and hasattr(self, 'job_id'):
+ delete_id = self.job_id
+ return self.tester.delete(FORMAT_STRING.
+ format(self.url, utils.SERVER_GROUP,
+ self.server_id, self.db_id,
+ DBMS_JOB_SCHEDULER_ID, delete_id),
+ data=json.dumps(self.data),
+ content_type=CONTENT_TYPE)
+
+
+def api_put(self, update_id):
+ return self.tester.put(FORMAT_STRING.
+ format(self.url, utils.SERVER_GROUP,
+ self.server_id, self.db_id,
+ DBMS_JOB_SCHEDULER_ID, update_id),
+ data=json.dumps(self.data),
+ content_type=CONTENT_TYPE)
+
+
+def api_get(self, get_id=None):
+ if get_id is None and hasattr(self, 'schedule_id'):
+ get_id = self.schedule_id
+ elif get_id is None and hasattr(self, 'program_id'):
+ get_id = self.program_id
+ elif get_id is None and hasattr(self, 'job_id'):
+ get_id = self.job_id
+
+ return self.tester.get(FORMAT_STRING.
+ format(self.url, utils.SERVER_GROUP,
+ self.server_id, self.db_id,
+ DBMS_JOB_SCHEDULER_ID, get_id),
+ content_type=CONTENT_TYPE)
+
+
+def api_get_msql(self, url_encode_data):
+ return self.tester.get("{0}{1}/{2}/{3}/{4}/?{5}".
+ format(self.url, utils.SERVER_GROUP,
+ self.server_id, self.db_id,
+ DBMS_JOB_SCHEDULER_ID,
+ urlencode(url_encode_data)),
+ data=json.dumps(self.data),
+ follow_redirects=True)
+
+
+def create_test_database(self):
+ """
+ This function is used to create a separate database to test DBMS Scheduler
+ """
+ # Drop database if already exists
+ connection = utils.get_db_connection(self.server['db'],
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ utils.drop_database(connection, TEST_DATABASE)
+
+ # Create a new database to test DBMS Scheduler
+ did = utils.create_database(self.server, TEST_DATABASE)
+ return TEST_DATABASE, did
+
+
+def is_supported_version(self):
+ """
+ This function is used to check whether version is supported to run tests.
+ """
+ return (self.server_information['type'] == 'ppas' and
+ self.server_information['server_version'] >= 160000)
+
+
+def is_dbms_job_scheduler_present(self):
+ """
+ This function is used to check the DBMS Job Scheduler is installed.
+ """
+ try:
+ connection = utils.get_db_connection(self.db_name,
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ pg_cursor = connection.cursor()
+
+ query = """SELECT COUNT(*) FROM pg_extension WHERE extname IN
+ ('edb_job_scheduler', 'dbms_scheduler')"""
+ pg_cursor.execute(query)
+ res = pg_cursor.fetchone()
+
+ if res and len(res) > 0 and int(res[0]) == 2:
+ # Get the list of databases specified for the edb_job_scheduler
+ pg_cursor.execute("""SHOW edb_job_scheduler.database_list""")
+ res = pg_cursor.fetchone()
+ # If database is available in the specified list than return
+ # True.
+ if res and len(res) > 0 and self.db_name in res[0]:
+ return True
+ connection.close()
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
+
+ return False
+
+
+def create_job_scheduler_extensions(self):
+ """
+ This function is used to create the extension for DBMS Job Scheduler.
+ """
+ try:
+ connection = utils.get_db_connection(self.db_name,
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ pg_cursor = connection.cursor()
+
+ # Create edb_job_scheduler extension if not exist.
+ pg_cursor.execute('''CREATE EXTENSION IF NOT EXISTS
+ "edb_job_scheduler"''')
+
+ # Create dbms_scheduler extension if not exist.
+ pg_cursor.execute('''CREATE EXTENSION IF NOT EXISTS
+ "dbms_scheduler"''')
+
+ connection.commit()
+ connection.close()
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
+
+ return False
+
+
+def delete_job_scheduler_extensions(self):
+ """
+ This function is used to create the extension for DBMS Job Scheduler.
+ """
+ try:
+ connection = utils.get_db_connection(self.db_name,
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ pg_cursor = connection.cursor()
+
+ # Drop edb_job_scheduler extension if exist.
+ pg_cursor.execute('''DROP EXTENSION IF EXISTS
+ "edb_job_scheduler" CASCADE''')
+
+ # Drop dbms_scheduler extension if exist.
+ pg_cursor.execute('''DROP EXTENSION IF EXISTS
+ "dbms_scheduler" CASCADE''')
+
+ connection.commit()
+ connection.close()
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
+
+
+def verify_dbms_schedule(self, sch_name):
+ """
+ This function is used to verify the DBMS schedule is created or not
+ """
+ try:
+ connection = utils.get_db_connection(self.db_name,
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ pg_cursor = connection.cursor()
+ sql = """SELECT
+ COUNT(*) FROM sys.scheduler_0300_schedule
+ WHERE dss_schedule_name = '{0}'""".format(sch_name)
+
+ pg_cursor.execute(sql)
+ res = pg_cursor.fetchone()
+ connection.close()
+
+ if res and len(res) > 0 and int(res[0]) >= 1:
+ return True
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
+
+ return False
+
+
+def create_dbms_schedule(self, sch_name):
+ """
+ This function is used to create the dbms schedule.
+ """
+ try:
+ connection = utils.get_db_connection(self.db_name,
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ pg_cursor = connection.cursor()
+ sql = """EXEC dbms_scheduler.CREATE_SCHEDULE(
+ schedule_name => '{0}',
+ start_date => '01-JUN-13 09:00:00.000000',
+ repeat_interval => 'FREQ=DAILY;BYDAY=MON,TUE,WED,THU,FRI;',
+ comments => 'This schedule executes weeknight at 5');
+ """.format(sch_name)
+ pg_cursor.execute(sql)
+ connection.commit()
+
+ pg_cursor.execute("""SELECT
+ dss_schedule_id FROM sys.scheduler_0300_schedule
+ WHERE dss_schedule_name = '{0}'""".format(sch_name))
+ res = pg_cursor.fetchone()
+ connection.close()
+
+ if res and len(res) > 0 and int(res[0]) >= 1:
+ return int(res[0])
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
+
+ return 0
+
+
+def delete_dbms_schedule(self, sch_name):
+ """
+ This function is used to delete the dbms schedule.
+ """
+ try:
+ connection = utils.get_db_connection(self.db_name,
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ pg_cursor = connection.cursor()
+ sql = """EXEC dbms_scheduler.DROP_SCHEDULE('{0}')""".format(
+ sch_name)
+ pg_cursor.execute(sql)
+
+ connection.commit()
+ connection.close()
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
+
+
+def verify_dbms_program(self, prg_name):
+ """
+ This function is used to verify the DBMS program is created or not
+ """
+ try:
+ connection = utils.get_db_connection(self.db_name,
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ pg_cursor = connection.cursor()
+ sql = """SELECT
+ COUNT(*) FROM sys.scheduler_0200_program
+ WHERE dsp_program_name = '{0}'""".format(prg_name)
+
+ pg_cursor.execute(sql)
+ res = pg_cursor.fetchone()
+ connection.close()
+
+ if res and len(res) > 0 and int(res[0]) >= 1:
+ return True
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
+
+ return False
+
+
+def create_dbms_program(self, prg_name, enabled=True,
+ with_proc=False, proc_name=None, define_args=False):
+ """
+ This function is used to create the dbms program.
+ """
+ try:
+ connection = utils.get_db_connection(self.db_name,
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ pg_cursor = connection.cursor()
+ if with_proc:
+ sql = """EXEC dbms_scheduler.CREATE_PROGRAM(
+ program_name => '{0}',
+ program_type => 'STORED_PROCEDURE',
+ program_action => '{1}',
+ enabled => {2},
+ comments => 'This is a program with procedure');
+ """.format(prg_name, proc_name, enabled)
+
+ if define_args:
+ sql += """ EXEC dbms_scheduler.DEFINE_PROGRAM_ARGUMENT(
+ program_name => '{0}',
+ argument_position => 0,
+ argument_name => 'salary',
+ argument_type => 'bigint',
+ default_value => '10000');
+ """.format(prg_name)
+ else:
+ sql = """EXEC dbms_scheduler.CREATE_PROGRAM(
+ program_name => '{0}',
+ program_type => 'PLSQL_BLOCK',
+ program_action => 'BEGIN SELECT 1; END;',
+ enabled => {1},
+ comments => 'This is a test program with plsql');
+ """.format(prg_name, enabled)
+ pg_cursor.execute(sql)
+ connection.commit()
+
+ pg_cursor.execute("""SELECT
+ dsp_program_id FROM sys.scheduler_0200_program
+ WHERE dsp_program_name = '{0}'""".format(prg_name))
+ res = pg_cursor.fetchone()
+ connection.close()
+
+ if res and len(res) > 0 and int(res[0]) >= 1:
+ return int(res[0])
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
+
+ return 0
+
+
+def delete_dbms_program(self, prg_name):
+ """
+ This function is used to delete the dbms program.
+ """
+ try:
+ connection = utils.get_db_connection(self.db_name,
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ pg_cursor = connection.cursor()
+ sql = """EXEC dbms_scheduler.DROP_PROGRAM('{0}')""".format(
+ prg_name)
+ pg_cursor.execute(sql)
+
+ connection.commit()
+ connection.close()
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
+
+
+def verify_dbms_job(self, job_name):
+ """
+ This function is used to verify the DBMS job is created or not
+ """
+ try:
+ connection = utils.get_db_connection(self.db_name,
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ pg_cursor = connection.cursor()
+ sql = """SELECT
+ COUNT(*) FROM sys.scheduler_0400_job
+ WHERE dsj_job_name = '{0}'""".format(job_name)
+
+ pg_cursor.execute(sql)
+ res = pg_cursor.fetchone()
+ connection.close()
+
+ if res and len(res) > 0 and int(res[0]) >= 1:
+ return True
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
+
+ return False
+
+
+def create_dbms_job(self, job_name, with_proc=False, prg_name=None,
+ sch_name=None):
+ """
+ This function is used to create the dbms program.
+ """
+ try:
+ connection = utils.get_db_connection(self.db_name,
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ pg_cursor = connection.cursor()
+ if with_proc:
+ sql = """EXEC dbms_scheduler.CREATE_JOB(
+ job_name => '{0}',
+ program_name => '{1}',
+ schedule_name => '{2}');
+ """.format(job_name, prg_name, sch_name)
+ else:
+ sql = """EXEC dbms_scheduler.CREATE_JOB(
+ job_name => '{0}',
+ job_type => 'PLSQL_BLOCK',
+ job_action => 'BEGIN PERFORM 1; END;',
+ repeat_interval => 'FREQ=YEARLY;',
+ start_date => '2024-02-27 00:00:00 +05:30');
+ """.format(job_name)
+ pg_cursor.execute(sql)
+ connection.commit()
+
+ pg_cursor.execute("""SELECT
+ dsj_job_id FROM sys.scheduler_0400_job
+ WHERE dsj_job_name = '{0}'""".format(job_name))
+ res = pg_cursor.fetchone()
+ connection.close()
+
+ if res and len(res) > 0 and int(res[0]) >= 1:
+ return int(res[0])
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
+
+ return 0
+
+
+def delete_dbms_job(self, job_name):
+ """
+ This function is used to delete the dbms job.
+ """
+ try:
+ connection = utils.get_db_connection(self.db_name,
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ pg_cursor = connection.cursor()
+ sql = """EXEC dbms_scheduler.DROP_JOB('{0}')""".format(
+ job_name)
+ pg_cursor.execute(sql)
+
+ connection.commit()
+ connection.close()
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
+
+
+def clean_up(self):
+ # Delete extension required for job scheduler
+ delete_job_scheduler_extensions(self)
+ database_utils.disconnect_database(self, self.server_id, self.db_id)
+
+ # Drop database if already exists
+ connection = utils.get_db_connection(self.server['db'],
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ utils.drop_database(connection, self.db_name)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/utils.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/utils.py
new file mode 100644
index 000000000..a9babfbe7
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/utils.py
@@ -0,0 +1,128 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""dbms job schedular utilities"""
+from pgadmin.browser.server_groups.servers.databases.schemas.functions.utils \
+ import format_arguments_from_db
+
+MONTHS_MAPPING = dict(
+ JAN='1', FEB='2', MAR='3', APR='4', MAY='5', JUN='6',
+ JUL='7', AUG='8', SEP='9', OCT='10', NOV='11', DEC='12'
+)
+WEEKDAY_MAPPING = dict(
+ MON='1', TUE='2', WED='3', THU='4', FRI='5', SAT='6', SUN='7'
+)
+# Required for reverse mapping
+MONTHS_MAPPING_REV = {v: k for k, v in MONTHS_MAPPING.items()}
+WEEKDAY_MAPPING_REV = {v: k for k, v in WEEKDAY_MAPPING.items()}
+
+
+def resolve_calendar_string(calendar_string):
+ """
+ Converts calendar_string to data
+ Args:
+ calendar_string: string to be converted
+ """
+ freq = None
+ by_date = None
+ by_month = []
+ by_monthday = []
+ by_weekday = []
+ by_hour = []
+ by_minute = []
+
+ if calendar_string is not None and len(calendar_string) > 0:
+ # First split on the basis of semicolon
+ cal_list = calendar_string.split(';')
+ for token in cal_list:
+ # Split on the basis of '=' operator as tokens are
+ # like FREQ=MONTHLY
+ if not token.strip():
+ continue
+ [token_name, token_value] = token.split('=')
+ token_name = token_name.strip().upper()
+ token_value = token_value.strip()
+
+ if token_name == 'FREQ':
+ freq = token_value
+ elif token_name == 'BYDATE':
+ by_date = token_value
+ elif token_name == 'BYMONTH':
+ by_month = [MONTHS_MAPPING.get(v.upper(), v)
+ for v in token_value.split(',')]
+ elif token_name == 'BYMONTHDAY':
+ by_monthday = token_value.split(',')
+ elif token_name == 'BYDAY':
+ by_weekday = [WEEKDAY_MAPPING.get(v.upper(), v)
+ for v in token_value.split(',')]
+ elif token_name == 'BYHOUR':
+ by_hour = token_value.split(',')
+ elif token_name == 'BYMINUTE':
+ by_minute = token_value.split(',')
+
+ return freq, by_date, by_month, by_monthday, by_weekday, by_hour, by_minute
+
+
+def create_calendar_string(frequency, date, months, monthdays, weekdays,
+ hours, minutes):
+ """
+ Create calendar string based on the given value
+
+ Args:
+ frequency:
+ date:
+ months:
+ monthdays:
+ weekdays:
+ hours:
+ minutes:
+ """
+ calendar_str = ''
+
+ if frequency is not None:
+ calendar_str = 'FREQ=' + frequency + ';'
+ if date is not None:
+ calendar_str += 'BYDATE=' + str(date) + ';'
+ if months is not None and isinstance(months, list) and len(months) > 0:
+ months = [MONTHS_MAPPING_REV.get(v, v) for v in months]
+ calendar_str += 'BYMONTH=' + ','.join(months) + ';'
+ if (monthdays is not None and isinstance(monthdays, list) and
+ len(monthdays) > 0):
+ calendar_str += 'BYMONTHDAY=' + ','.join(monthdays) + ';'
+ if (weekdays is not None and isinstance(weekdays, list) and
+ len(weekdays) > 0):
+ weekdays = [WEEKDAY_MAPPING_REV.get(v, v) for v in weekdays]
+ calendar_str += 'BYDAY=' + ','.join(weekdays) + ';'
+ if hours is not None and isinstance(hours, list) and len(hours) > 0:
+ calendar_str += 'BYHOUR=' + ','.join(hours) + ';'
+ if minutes is not None and isinstance(minutes, list) and len(minutes) > 0:
+ calendar_str += 'BYMINUTE=' + ','.join(minutes)
+
+ return calendar_str
+
+
+def get_formatted_program_args(template_path, conn, data):
+ """
+ This function is used to formate the program arguments.
+ Args:
+ template_path:
+ conn:
+ data:
+
+ Returns:
+
+ """
+ if 'jsprnoofargs' in data and data['jsprnoofargs'] > 0:
+ frmtd_params, _ = format_arguments_from_db(
+ template_path, conn, data)
+
+ if 'arguments' in frmtd_params:
+ new_args = [item for item in frmtd_params['arguments']
+ if item['argname'] is not None]
+ data['jsprarguments'] = new_args
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/__init__.py
index 4edccb89a..09263e18d 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/__init__.py
@@ -9,15 +9,12 @@
"""Implements Functions/Procedures Node."""
-import copy
import re
import sys
-import traceback
from functools import wraps
import json
-from flask import render_template, request, jsonify, \
- current_app
+from flask import render_template, request, jsonify
from flask_babel import gettext
from pgadmin.browser.server_groups.servers import databases
@@ -34,6 +31,8 @@ from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
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 pgadmin.browser.server_groups.servers.databases.schemas.functions.utils \
+ import format_arguments_from_db
class FunctionModule(SchemaChildModule):
@@ -475,235 +474,6 @@ class FunctionView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare):
status=200
)
- def _get_argument_values(self, data):
- """
- This function is used to get the argument values for
- function/procedure.
- :param data:
- :return:
- """
- proargtypes = [ptype for ptype in data['proargtypenames'].split(",")] \
- if data['proargtypenames'] else []
- proargmodes = data['proargmodes'] if data['proargmodes'] else \
- ['i'] * len(proargtypes)
- proargnames = data['proargnames'] if data['proargnames'] else []
- proargdefaultvals = re.split(
- r',(?=(?:[^\"\']*[\"\'][^\"\']*[\"\'])*[^\"\']*$)',
- data['proargdefaultvals']) if data['proargdefaultvals'] else []
- proallargtypes = data['proallargtypes'] \
- if data['proallargtypes'] else []
-
- return {'proargtypes': proargtypes, 'proargmodes': proargmodes,
- 'proargnames': proargnames,
- 'proargdefaultvals': proargdefaultvals,
- 'proallargtypes': proallargtypes}
-
- def _params_list_for_display(self, proargmodes_fltrd, proargtypes,
- proargnames, proargdefaultvals):
- """
- This function is used to prepare dictionary of arguments to
- display on UI.
- :param proargmodes_fltrd:
- :param proargtypes:
- :param proargnames:
- :param proargdefaultvals:
- :return:
- """
- # Insert null value against the parameters which do not have
- # default values.
- if len(proargmodes_fltrd) > len(proargdefaultvals):
- dif = len(proargmodes_fltrd) - len(proargdefaultvals)
- while dif > 0:
- proargdefaultvals.insert(0, '')
- dif -= 1
-
- param = {"arguments": [
- self._map_arguments_dict(
- i, proargmodes_fltrd[i] if len(proargmodes_fltrd) > i else '',
- proargtypes[i] if len(proargtypes) > i else '',
- proargnames[i] if len(proargnames) > i else '',
- proargdefaultvals[i] if len(proargdefaultvals) > i else ''
- )
- for i in range(len(proargtypes))]}
- return param
-
- def _display_properties_argument_list(self, proargmodes_fltrd,
- proargtypes, proargnames,
- proargdefaultvals):
- """
- This function is used to prepare list of arguments to display on UI.
- :param proargmodes_fltrd:
- :param proargtypes:
- :param proargnames:
- :param proargdefaultvals:
- :return:
- """
- proargs = [self._map_arguments_list(
- proargmodes_fltrd[i] if len(proargmodes_fltrd) > i else '',
- proargtypes[i] if len(proargtypes) > i else '',
- proargnames[i] if len(proargnames) > i else '',
- proargdefaultvals[i] if len(proargdefaultvals) > i else ''
- )
- for i in range(len(proargtypes))]
-
- return proargs
-
- def _format_arguments_from_db(self, data):
- """
- Create Argument list of the Function.
-
- Args:
- data: Function Data
-
- Returns:
- Function Arguments in the following format.
- [
- {'proargtypes': 'integer', 'proargmodes: 'IN',
- 'proargnames': 'column1', 'proargdefaultvals': 1}, {...}
- ]
- Where
- Arguments:
- # proargtypes: Argument Types (Data Type)
- # proargmodes: Argument Modes [IN, OUT, INOUT, VARIADIC]
- # proargnames: Argument Name
- # proargdefaultvals: Default Value of the Argument
- """
- arguments = self._get_argument_values(data)
- proargtypes = arguments['proargtypes']
- proargmodes = arguments['proargmodes']
- proargnames = arguments['proargnames']
- proargdefaultvals = arguments['proargdefaultvals']
- proallargtypes = arguments['proallargtypes']
-
- proargmodenames = {
- 'i': 'IN', 'o': 'OUT', 'b': 'INOUT', 'v': 'VARIADIC', 't': 'TABLE'
- }
-
- # We need to put default parameter at proper location in list
- # Total number of default parameters
- total_default_parameters = len(proargdefaultvals)
-
- # Total number of parameters
- total_parameters = len(proargtypes)
-
- # Parameters which do not have default parameters
- non_default_parameters = total_parameters - total_default_parameters
-
- # only if we have at least one parameter with default value
- if total_default_parameters > 0 and non_default_parameters > 0:
- for idx in range(non_default_parameters):
- # Set null value for parameter non-default parameter
- proargdefaultvals.insert(idx, '')
-
- # The proargtypes doesn't give OUT params, so we need to fetch
- # those from database explicitly, below code is written for this
- # purpose.
- #
- # proallargtypes gives all the Function's argument including OUT,
- # but we have not used that column; as the data type of this
- # column (i.e. oid[]) is not supported by oidvectortypes(oidvector)
- # function which we have used to fetch the datatypes
- # of the other parameters.
-
- proargmodes_fltrd = copy.deepcopy(proargmodes)
- proargnames_fltrd = []
- cnt = 0
- for m in proargmodes:
- if m == 'o': # Out Mode
- sql = render_template("/".join([self.sql_template_path,
- 'get_out_types.sql']),
- out_arg_oid=proallargtypes[cnt])
- status, out_arg_type = self.conn.execute_scalar(sql)
- if not status:
- return internal_server_error(errormsg=out_arg_type)
-
- # Insert out parameter datatype
- proargtypes.insert(cnt, out_arg_type)
- proargdefaultvals.insert(cnt, '')
- elif m == 'v': # Variadic Mode
- proargdefaultvals.insert(cnt, '')
- elif m == 't': # Table Mode
- proargmodes_fltrd.remove(m)
- proargnames_fltrd.append(proargnames[cnt])
-
- cnt += 1
-
- cnt = 0
- # Map param's short form to its actual name. (ex: 'i' to 'IN')
- for m in proargmodes_fltrd:
- proargmodes_fltrd[cnt] = proargmodenames[m]
- cnt += 1
-
- # Removes Argument Names from the list if that argument is removed
- # from the list
- for i in proargnames_fltrd:
- proargnames.remove(i)
-
- # Prepare list of Argument list dict to be displayed in the Data Grid.
- params = self._params_list_for_display(proargmodes_fltrd, proargtypes,
- proargnames, proargdefaultvals)
-
- # Prepare string formatted Argument to be displayed in the Properties
- # panel.
- proargs = self._display_properties_argument_list(proargmodes_fltrd,
- proargtypes,
- proargnames,
- proargdefaultvals)
-
- proargs = {"proargs": ", ".join(proargs)}
-
- return params, proargs
-
- def _map_arguments_dict(self, argid, argmode, argtype, argname, argdefval):
- """
- Returns Dict of formatted Arguments.
- Args:
- argid: Argument Sequence Number
- argmode: Argument Mode
- argname: Argument Name
- argtype: Argument Type
- argdef: Argument Default Value
- """
- # The pg_get_expr(proargdefaults, 'pg_catalog.pg_class'::regclass) SQL
- # statement gives us '-' as a default value for INOUT mode.
- # so, replacing it with empty string.
- if argmode == 'INOUT' and argdefval.strip() == '-':
- argdefval = ''
-
- return {"argid": argid,
- "argtype": argtype.strip() if argtype is not None else '',
- "argmode": argmode,
- "argname": argname,
- "argdefval": argdefval}
-
- def _map_arguments_list(self, argmode, argtype, argname, argdef):
- """
- Returns List of formatted Arguments.
- Args:
- argmode: Argument Mode
- argname: Argument Name
- argtype: Argument Type
- argdef: Argument Default Value
- """
- # The pg_get_expr(proargdefaults, 'pg_catalog.pg_class'::regclass) SQL
- # statement gives us '-' as a default value for INOUT mode.
- # so, replacing it with empty string.
- if argmode == 'INOUT' and argdef.strip() == '-':
- argdef = ''
-
- arg = ''
-
- if argmode:
- arg += argmode + " "
- if argname:
- arg += argname + " "
- if argtype:
- arg += argtype + " "
- if argdef:
- arg += " DEFAULT " + argdef
-
- return arg.strip(" ")
-
def _format_proacl_from_db(self, proacl):
"""
Returns privileges.
@@ -1581,7 +1351,9 @@ class FunctionView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare):
resp_data = res['rows'][0]
# Get formatted Arguments
- frmtd_params, frmtd_proargs = self._format_arguments_from_db(resp_data)
+ frmtd_params, frmtd_proargs = (
+ format_arguments_from_db(self.sql_template_path, self.conn,
+ resp_data))
resp_data.update(frmtd_params)
resp_data.update(frmtd_proargs)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/utils.py
new file mode 100644
index 000000000..2ba6fdfa9
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/utils.py
@@ -0,0 +1,262 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import copy
+import re
+from flask import render_template
+from pgadmin.utils.ajax import internal_server_error
+
+
+def get_argument_values(data):
+ """
+ This function is used to get the argument values for
+ function/procedure.
+ :param data:
+ :return:
+ """
+ proargtypes = []
+ if ('proargtypenames' in data and data['proargtypenames'] and
+ isinstance(data['proargtypenames'], str)):
+ proargtypes = [ptype for ptype in data['proargtypenames'].split(",")]
+ elif 'proargtypenames' in data and data['proargtypenames']:
+ proargtypes = data['proargtypenames']
+
+ proargmodes = (
+ data)['proargmodes'] if 'proargmodes' in data and data['proargmodes'] \
+ else ['i'] * len(proargtypes)
+ proargnames = (
+ data)['proargnames'] if 'proargnames' in data and data['proargnames'] \
+ else []
+
+ proargdefaultvals = []
+ if ('proargdefaultvals' in data and data['proargdefaultvals'] and
+ isinstance(data['proargdefaultvals'], str)):
+ proargdefaultvals = re.split(
+ r',(?=(?:[^\"\']*[\"\'][^\"\']*[\"\'])*[^\"\']*$)',
+ data['proargdefaultvals'])
+ elif 'proargdefaultvals' in data and data['proargdefaultvals']:
+ proargdefaultvals = data['proargdefaultvals']
+
+ proallargtypes = data['proallargtypes'] \
+ if 'proallargtypes' in data and data['proallargtypes'] else []
+
+ return {'proargtypes': proargtypes, 'proargmodes': proargmodes,
+ 'proargnames': proargnames,
+ 'proargdefaultvals': proargdefaultvals,
+ 'proallargtypes': proallargtypes}
+
+
+def params_list_for_display(proargmodes_fltrd, proargtypes,
+ proargnames, proargdefaultvals):
+ """
+ This function is used to prepare dictionary of arguments to
+ display on UI.
+ :param proargmodes_fltrd:
+ :param proargtypes:
+ :param proargnames:
+ :param proargdefaultvals:
+ :return:
+ """
+ # Insert null value against the parameters which do not have
+ # default values.
+ if len(proargmodes_fltrd) > len(proargdefaultvals):
+ dif = len(proargmodes_fltrd) - len(proargdefaultvals)
+ while dif > 0:
+ proargdefaultvals.insert(0, '')
+ dif -= 1
+
+ param = {"arguments": [
+ map_arguments_dict(
+ i, proargmodes_fltrd[i] if len(proargmodes_fltrd) > i else '',
+ proargtypes[i] if len(proargtypes) > i else '',
+ proargnames[i] if len(proargnames) > i else '',
+ proargdefaultvals[i] if len(proargdefaultvals) > i else ''
+ )
+ for i in range(len(proargtypes))]}
+ return param
+
+
+def display_properties_argument_list(proargmodes_fltrd, proargtypes,
+ proargnames, proargdefaultvals):
+ """
+ This function is used to prepare list of arguments to display on UI.
+ :param proargmodes_fltrd:
+ :param proargtypes:
+ :param proargnames:
+ :param proargdefaultvals:
+ :return:
+ """
+ proargs = [map_arguments_list(
+ proargmodes_fltrd[i] if len(proargmodes_fltrd) > i else '',
+ proargtypes[i] if len(proargtypes) > i else '',
+ proargnames[i] if len(proargnames) > i else '',
+ proargdefaultvals[i] if len(proargdefaultvals) > i else ''
+ )
+ for i in range(len(proargtypes))]
+
+ return proargs
+
+
+def map_arguments_dict(argid, argmode, argtype, argname, argdefval):
+ """
+ Returns Dict of formatted Arguments.
+ Args:
+ argid: Argument Sequence Number
+ argmode: Argument Mode
+ argname: Argument Name
+ argtype: Argument Type
+ argdefval: Argument Default Value
+ """
+ # The pg_get_expr(proargdefaults, 'pg_catalog.pg_class'::regclass) SQL
+ # statement gives us '-' as a default value for INOUT mode.
+ # so, replacing it with empty string.
+ if argmode == 'INOUT' and argdefval.strip() == '-':
+ argdefval = ''
+
+ return {"argid": argid,
+ "argtype": argtype.strip() if argtype is not None else '',
+ "argmode": argmode,
+ "argname": argname,
+ "argdefval": argdefval}
+
+
+def map_arguments_list(argmode, argtype, argname, argdef):
+ """
+ Returns List of formatted Arguments.
+ Args:
+ argmode: Argument Mode
+ argname: Argument Name
+ argtype: Argument Type
+ argdef: Argument Default Value
+ """
+ # The pg_get_expr(proargdefaults, 'pg_catalog.pg_class'::regclass) SQL
+ # statement gives us '-' as a default value for INOUT mode.
+ # so, replacing it with empty string.
+ if argmode == 'INOUT' and argdef.strip() == '-':
+ argdef = ''
+
+ arg = ''
+
+ if argmode:
+ arg += argmode + " "
+ if argname:
+ arg += argname + " "
+ if argtype:
+ arg += argtype + " "
+ if argdef:
+ arg += " DEFAULT " + argdef
+
+ return arg.strip(" ")
+
+
+def format_arguments_from_db(sql_template_path, conn, data):
+ """
+ Create Argument list of the Function.
+
+ Args:
+ sql_template_path:
+ conn:
+ data: Function Data
+
+ Returns:
+ Function Arguments in the following format.
+ [
+ {'proargtypes': 'integer', 'proargmodes: 'IN',
+ 'proargnames': 'column1', 'proargdefaultvals': 1}, {...}
+ ]
+ Where
+ Arguments:
+ # proargtypes: Argument Types (Data Type)
+ # proargmodes: Argument Modes [IN, OUT, INOUT, VARIADIC]
+ # proargnames: Argument Name
+ # proargdefaultvals: Default Value of the Argument
+ """
+ arguments = get_argument_values(data)
+ proargtypes = arguments['proargtypes']
+ proargmodes = arguments['proargmodes']
+ proargnames = arguments['proargnames']
+ proargdefaultvals = arguments['proargdefaultvals']
+ proallargtypes = arguments['proallargtypes']
+
+ proargmodenames = {
+ 'i': 'IN', 'o': 'OUT', 'b': 'INOUT', 'v': 'VARIADIC', 't': 'TABLE'
+ }
+
+ # We need to put default parameter at proper location in list
+ # Total number of default parameters
+ total_default_parameters = len(proargdefaultvals)
+
+ # Total number of parameters
+ total_parameters = len(proargtypes)
+
+ # Parameters which do not have default parameters
+ non_default_parameters = total_parameters - total_default_parameters
+
+ # only if we have at least one parameter with default value
+ if total_default_parameters > 0 and non_default_parameters > 0:
+ for idx in range(non_default_parameters):
+ # Set null value for parameter non-default parameter
+ proargdefaultvals.insert(idx, '')
+
+ # The proargtypes doesn't give OUT params, so we need to fetch
+ # those from database explicitly, below code is written for this
+ # purpose.
+ #
+ # proallargtypes gives all the Function's argument including OUT,
+ # but we have not used that column; as the data type of this
+ # column (i.e. oid[]) is not supported by oidvectortypes(oidvector)
+ # function which we have used to fetch the datatypes
+ # of the other parameters.
+
+ proargmodes_fltrd = copy.deepcopy(proargmodes)
+ proargnames_fltrd = []
+ cnt = 0
+ for m in proargmodes:
+ if m == 'o': # Out Mode
+ sql = render_template("/".join([sql_template_path,
+ 'get_out_types.sql']),
+ out_arg_oid=proallargtypes[cnt])
+ status, out_arg_type = conn.execute_scalar(sql)
+ if not status:
+ return internal_server_error(errormsg=out_arg_type)
+
+ # Insert out parameter datatype
+ proargtypes.insert(cnt, out_arg_type)
+ proargdefaultvals.insert(cnt, '')
+ elif m == 'v': # Variadic Mode
+ proargdefaultvals.insert(cnt, '')
+ elif m == 't': # Table Mode
+ proargmodes_fltrd.remove(m)
+ proargnames_fltrd.append(proargnames[cnt])
+
+ cnt += 1
+
+ cnt = 0
+ # Map param's short form to its actual name. (ex: 'i' to 'IN')
+ for m in proargmodes_fltrd:
+ proargmodes_fltrd[cnt] = proargmodenames[m]
+ cnt += 1
+
+ # Removes Argument Names from the list if that argument is removed
+ # from the list
+ for i in proargnames_fltrd:
+ proargnames.remove(i)
+
+ # Prepare list of Argument list dict to be displayed in the Data Grid.
+ params = params_list_for_display(proargmodes_fltrd, proargtypes,
+ proargnames, proargdefaultvals)
+
+ # Prepare string formatted Argument to be displayed in the Properties
+ # panel.
+ proargs = display_properties_argument_list(proargmodes_fltrd, proargtypes,
+ proargnames, proargdefaultvals)
+
+ proargs = {"proargs": ", ".join(proargs)}
+
+ return params, proargs
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/12_plus/create_column_identity_for_restart_seq.msql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/12_plus/create_column_identity_for_restart_seq.msql
index 4f2e5ccc6..a9cc7a30e 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/12_plus/create_column_identity_for_restart_seq.msql
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/12_plus/create_column_identity_for_restart_seq.msql
@@ -1,5 +1,5 @@
ALTER TABLE IF EXISTS testschema."table_3_$%{}[]()&*^!@""'`\/#"
- ADD COLUMN "col_6_$%{}[]()&*^!@""'`\/#" bigint(None, None) NOT NULL GENERATED BY DEFAULT AS IDENTITY ( CYCLE INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 10 CACHE 1 );
+ ADD COLUMN "col_6_$%{}[]()&*^!@""'`\/#" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( CYCLE INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 10 CACHE 1 );
COMMENT ON COLUMN testschema."table_3_$%{}[]()&*^!@""'`\/#"."col_6_$%{}[]()&*^!@""'`\/#"
IS 'demo comments';
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/default/alter_column_char.msql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/default/alter_column_char.msql
index 5a232ed75..a5398f97e 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/default/alter_column_char.msql
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/default/alter_column_char.msql
@@ -2,7 +2,7 @@ ALTER TABLE IF EXISTS testschema."table_2_$%{}[]()&*^!@""'`\/#"
RENAME "col_2_$%{}[]()&*^!@""'`\/#" TO "new_col_2_$%{}[]()&*^!@""'`\/#";
ALTER TABLE testschema."table_2_$%{}[]()&*^!@""'`\/#"
- ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" TYPE character(None) COLLATE pg_catalog."C";
+ ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" TYPE character COLLATE pg_catalog."C";
ALTER TABLE IF EXISTS testschema."table_2_$%{}[]()&*^!@""'`\/#"
ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" SET STATISTICS 5;
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/12_plus/alter_column_char.msql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/12_plus/alter_column_char.msql
index 78412bcb9..b34f0e9fb 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/12_plus/alter_column_char.msql
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/12_plus/alter_column_char.msql
@@ -2,7 +2,7 @@ ALTER TABLE IF EXISTS testschema."table_3_$%{}[]()&*^!@""'`\/#"
RENAME "col_2_$%{}[]()&*^!@""'`\/#" TO "new_col_2_$%{}[]()&*^!@""'`\/#";
ALTER TABLE testschema."table_3_$%{}[]()&*^!@""'`\/#"
- ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" TYPE character(None) COLLATE pg_catalog."C";
+ ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" TYPE character COLLATE pg_catalog."C";
ALTER TABLE IF EXISTS testschema."table_3_$%{}[]()&*^!@""'`\/#"
ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" SET STATISTICS 5;
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/default/alter_column_char.msql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/default/alter_column_char.msql
index 5a232ed75..a5398f97e 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/default/alter_column_char.msql
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/default/alter_column_char.msql
@@ -2,7 +2,7 @@ ALTER TABLE IF EXISTS testschema."table_2_$%{}[]()&*^!@""'`\/#"
RENAME "col_2_$%{}[]()&*^!@""'`\/#" TO "new_col_2_$%{}[]()&*^!@""'`\/#";
ALTER TABLE testschema."table_2_$%{}[]()&*^!@""'`\/#"
- ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" TYPE character(None) COLLATE pg_catalog."C";
+ ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" TYPE character COLLATE pg_catalog."C";
ALTER TABLE IF EXISTS testschema."table_2_$%{}[]()&*^!@""'`\/#"
ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" SET STATISTICS 5;
diff --git a/web/pgadmin/browser/server_groups/servers/pgagent/schedules/__init__.py b/web/pgadmin/browser/server_groups/servers/pgagent/schedules/__init__.py
index 6cc7a7c15..79af7e125 100644
--- a/web/pgadmin/browser/server_groups/servers/pgagent/schedules/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/pgagent/schedules/__init__.py
@@ -113,7 +113,7 @@ class JobScheduleView(PGChildNodeView):
Methods:
-------
* __init__(**kwargs)
- - Method is used to initialize the JobScheduleView and it's base view.
+ - Method is used to initialize the JobScheduleView, and it's base view.
* check_precondition()
- This function will behave as a decorator which will checks
@@ -125,7 +125,7 @@ class JobScheduleView(PGChildNodeView):
collection.
* nodes()
- - This function will used to create all the child node within that
+ - This function will use to create all the child node within that
collection. Here it will create all the schedule node.
* properties(gid, sid, jid, jscid)
diff --git a/web/pgadmin/browser/server_groups/servers/pgagent/schedules/static/js/pga_schedule.ui.js b/web/pgadmin/browser/server_groups/servers/pgagent/schedules/static/js/pga_schedule.ui.js
index 62e58c02d..9db15cda6 100644
--- a/web/pgadmin/browser/server_groups/servers/pgagent/schedules/static/js/pga_schedule.ui.js
+++ b/web/pgadmin/browser/server_groups/servers/pgagent/schedules/static/js/pga_schedule.ui.js
@@ -11,67 +11,7 @@ import gettext from 'sources/gettext';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import { isEmptyString } from 'sources/validators';
import moment from 'moment';
-
-const weekdays = [
- {label: gettext('Sunday'), value: 'Sunday'},
- {label: gettext('Monday'), value: 'Monday'},
- {label: gettext('Tuesday'), value: 'Tuesday'},
- {label: gettext('Wednesday'), value: 'Wednesday'},
- {label: gettext('Thursday'), value: 'Thursday'},
- {label: gettext('Friday'), value: 'Friday'},
- {label: gettext('Saturday'), value: 'Saturday'},
- ],
- monthdays = [
- {label: gettext('1st'), value: '1st'}, {label: gettext('2nd'), value: '2nd'},
- {label: gettext('3rd'), value: '3rd'}, {label: gettext('4th'), value: '4th'},
- {label: gettext('5th'), value: '5th'}, {label: gettext('6th'), value: '6th'},
- {label: gettext('7th'), value: '7th'}, {label: gettext('8th'), value: '8th'},
- {label: gettext('9th'), value: '9th'}, {label: gettext('10th'), value: '10th'},
- {label: gettext('11th'), value: '11th'}, {label: gettext('12th'), value: '12th'},
- {label: gettext('13th'), value: '13th'}, {label: gettext('14th'), value: '14th'},
- {label: gettext('15th'), value: '15th'}, {label: gettext('16th'), value: '16th'},
- {label: gettext('17th'), value: '17th'}, {label: gettext('18th'), value: '18th'},
- {label: gettext('19th'), value: '19th'}, {label: gettext('20th'), value: '20th'},
- {label: gettext('21st'), value: '21st'}, {label: gettext('22nd'), value: '22nd'},
- {label: gettext('23rd'), value: '23rd'}, {label: gettext('24th'), value: '24th'},
- {label: gettext('25th'), value: '25th'}, {label: gettext('26th'), value: '26th'},
- {label: gettext('27th'), value: '27th'}, {label: gettext('28th'), value: '28th'},
- {label: gettext('29th'), value: '29th'}, {label: gettext('30th'), value: '30th'},
- {label: gettext('31st'), value: '31st'}, {label: gettext('Last day'), value: 'Last Day'},
- ],
- months = [
- {label: gettext('January'),value: 'January'}, {label: gettext('February'),value: 'February'},
- {label: gettext('March'), value: 'March'}, {label: gettext('April'), value: 'April'},
- {label: gettext('May'), value: 'May'}, {label: gettext('June'), value: 'June'},
- {label: gettext('July'), value: 'July'}, {label: gettext('August'), value: 'August'},
- {label: gettext('September'), value: 'September'}, {label: gettext('October'), value: 'October'},
- {label: gettext('November'), value: 'November'}, {label: gettext('December'), value: 'December'},
- ],
- hours = [
- {label: gettext('00'), value: '00'}, {label: gettext('01'), value: '01'}, {label: gettext('02'), value: '02'}, {label: gettext('03'), value: '03'},
- {label: gettext('04'), value: '04'}, {label: gettext('05'), value: '05'}, {label: gettext('06'), value: '06'}, {label: gettext('07'), value: '07'},
- {label: gettext('08'), value: '08'}, {label: gettext('09'), value: '09'}, {label: gettext('10'), value: '10'}, {label: gettext('11'), value: '11'},
- {label: gettext('12'), value: '12'}, {label: gettext('13'), value: '13'}, {label: gettext('14'), value: '14'}, {label: gettext('15'), value: '15'},
- {label: gettext('16'), value: '16'}, {label: gettext('17'), value: '17'}, {label: gettext('18'), value: '18'}, {label: gettext('19'), value: '19'},
- {label: gettext('20'), value: '20'}, {label: gettext('21'), value: '21'}, {label: gettext('22'), value: '22'}, {label: gettext('23'), value: '23'},
- ],
- minutes = [
- {label: gettext('00'), value: '00'}, {label: gettext('01'), value: '01'}, {label: gettext('02'), value: '02'}, {label: gettext('03'), value: '03'},
- {label: gettext('04'), value: '04'}, {label: gettext('05'), value: '05'}, {label: gettext('06'), value: '06'}, {label: gettext('07'), value: '07'},
- {label: gettext('08'), value: '08'}, {label: gettext('09'), value: '09'}, {label: gettext('10'), value: '10'}, {label: gettext('11'), value: '11'},
- {label: gettext('12'), value: '12'}, {label: gettext('13'), value: '13'}, {label: gettext('14'), value: '14'}, {label: gettext('15'), value: '15'},
- {label: gettext('16'), value: '16'}, {label: gettext('17'), value: '17'}, {label: gettext('18'), value: '18'}, {label: gettext('19'), value: '19'},
- {label: gettext('20'), value: '20'}, {label: gettext('21'), value: '21'}, {label: gettext('22'), value: '22'}, {label: gettext('23'), value: '23'},
- {label: gettext('24'), value: '24'}, {label: gettext('25'), value: '25'}, {label: gettext('26'), value: '26'}, {label: gettext('27'), value: '27'},
- {label: gettext('28'), value: '28'}, {label: gettext('29'), value: '29'}, {label: gettext('30'), value: '30'}, {label: gettext('31'), value: '31'},
- {label: gettext('32'), value: '32'}, {label: gettext('33'), value: '33'}, {label: gettext('34'), value: '34'}, {label: gettext('35'), value: '35'},
- {label: gettext('36'), value: '36'}, {label: gettext('37'), value: '37'}, {label: gettext('38'), value: '38'}, {label: gettext('39'), value: '39'},
- {label: gettext('40'), value: '40'}, {label: gettext('41'), value: '41'}, {label: gettext('42'), value: '42'}, {label: gettext('43'), value: '43'},
- {label: gettext('44'), value: '44'}, {label: gettext('45'), value: '45'}, {label: gettext('46'), value: '46'}, {label: gettext('47'), value: '47'},
- {label: gettext('48'), value: '48'}, {label: gettext('49'), value: '49'}, {label: gettext('50'), value: '50'}, {label: gettext('51'), value: '51'},
- {label: gettext('52'), value: '52'}, {label: gettext('53'), value: '53'}, {label: gettext('54'), value: '54'}, {label: gettext('55'), value: '55'},
- {label: gettext('56'), value: '56'}, {label: gettext('57'), value: '57'}, {label: gettext('58'), value: '58'}, {label: gettext('59'), value: '59'},
- ];
+import { WEEKDAYS, MONTHDAYS, MONTHS, HOURS, MINUTES } from '../../../../../../static/js/constants';
export class ExceptionsSchema extends BaseUISchema {
constructor(fieldOptions={}, initValues={}) {
@@ -178,7 +118,7 @@ export class DaysSchema extends BaseUISchema {
placeholder: gettext('Select the weekdays...'),
formatter: BooleanArrayFormatter,
},
- options: weekdays,
+ options: WEEKDAYS,
}, {
id: 'jscmonthdays', label: gettext('Month Days'), type: 'select',
group: gettext('Days'),
@@ -186,7 +126,7 @@ export class DaysSchema extends BaseUISchema {
placeholder: gettext('Select the month days...'),
formatter: BooleanArrayFormatter,
},
- options: monthdays,
+ options: MONTHDAYS,
}, {
id: 'jscmonths', label: gettext('Months'), type: 'select',
group: gettext('Days'),
@@ -194,7 +134,7 @@ export class DaysSchema extends BaseUISchema {
placeholder: gettext('Select the months...'),
formatter: BooleanArrayFormatter,
},
- options: months,
+ options: MONTHS,
}
];
}
@@ -220,7 +160,7 @@ export class TimesSchema extends BaseUISchema {
placeholder: gettext('Select the hours...'),
formatter: BooleanArrayFormatter,
},
- options: hours,
+ options: HOURS,
}, {
id: 'jscminutes', label: gettext('Minutes'), type: 'select',
group: gettext('Times'),
@@ -228,7 +168,7 @@ export class TimesSchema extends BaseUISchema {
placeholder: gettext('Select the minutes...'),
formatter: BooleanArrayFormatter,
},
- options: minutes,
+ options: MINUTES,
}
];
}
@@ -244,11 +184,11 @@ export default class PgaJobScheduleSchema extends BaseUISchema {
jscenabled: true,
jscstart: null,
jscend: null,
- jscweekdays: _.map(weekdays, function() { return false; }),
- jscmonthdays: _.map(monthdays, function() { return false; }),
- jscmonths: _.map(months, function() { return false; }),
- jschours: _.map(hours, function() { return false; }),
- jscminutes: _.map(minutes, function() { return false; }),
+ jscweekdays: _.map(WEEKDAYS, function() { return false; }),
+ jscmonthdays: _.map(MONTHDAYS, function() { return false; }),
+ jscmonths: _.map(MONTHS, function() { return false; }),
+ jschours: _.map(HOURS, function() { return false; }),
+ jscminutes: _.map(MINUTES, function() { return false; }),
jscexceptions: [],
...initValues,
});
@@ -324,7 +264,7 @@ export default class PgaJobScheduleSchema extends BaseUISchema {
controlProps: {
formatter: {
fromRaw: (backendVal)=> {
- return obj.customFromRaw(backendVal, weekdays);
+ return obj.customFromRaw(backendVal, WEEKDAYS);
}
},
}
@@ -334,7 +274,7 @@ export default class PgaJobScheduleSchema extends BaseUISchema {
controlProps: {
formatter: {
fromRaw: (backendVal)=> {
- return obj.customFromRaw(backendVal, monthdays);
+ return obj.customFromRaw(backendVal, MONTHDAYS);
}
},
}
@@ -344,7 +284,7 @@ export default class PgaJobScheduleSchema extends BaseUISchema {
controlProps: {
formatter: {
fromRaw: (backendVal)=> {
- return obj.customFromRaw(backendVal, months);
+ return obj.customFromRaw(backendVal, MONTHS);
}
},
}
@@ -354,7 +294,7 @@ export default class PgaJobScheduleSchema extends BaseUISchema {
controlProps: {
formatter: {
fromRaw: (backendVal)=> {
- return obj.customFromRaw(backendVal, hours);
+ return obj.customFromRaw(backendVal, HOURS);
}
},
}
@@ -364,7 +304,7 @@ export default class PgaJobScheduleSchema extends BaseUISchema {
controlProps: {
formatter: {
fromRaw: (backendVal)=> {
- return obj.customFromRaw(backendVal, minutes);
+ return obj.customFromRaw(backendVal, MINUTES);
}
},
}
diff --git a/web/pgadmin/browser/server_groups/servers/pgagent/schedules/tests/utils.py b/web/pgadmin/browser/server_groups/servers/pgagent/schedules/tests/utils.py
index 3a2e14c49..13b020864 100644
--- a/web/pgadmin/browser/server_groups/servers/pgagent/schedules/tests/utils.py
+++ b/web/pgadmin/browser/server_groups/servers/pgagent/schedules/tests/utils.py
@@ -1,5 +1,12 @@
-import sys
-import traceback
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
import os
import json
from urllib.parse import urlencode
diff --git a/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_login_role.msql b/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_login_role.msql
index 54576e03b..3ebaab484 100644
--- a/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_login_role.msql
+++ b/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_login_role.msql
@@ -6,5 +6,4 @@ CREATE ROLE "Role1_$%{}[]()&*^!@""'`\/#" WITH
INHERIT
REPLICATION
BYPASSRLS
- CONNECTION LIMIT -1
- PASSWORD 'xxxxxx';
+ CONNECTION LIMIT -1;
diff --git a/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_role.msql b/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_role.msql
index 63630a79f..5d0c2026e 100644
--- a/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_role.msql
+++ b/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_role.msql
@@ -6,5 +6,4 @@ CREATE ROLE "Role1_$%{}[]()&*^!@""'`\/#" WITH
INHERIT
NOREPLICATION
NOBYPASSRLS
- CONNECTION LIMIT -1
- PASSWORD 'xxxxxx';
+ CONNECTION LIMIT -1;
diff --git a/web/pgadmin/browser/static/js/constants.js b/web/pgadmin/browser/static/js/constants.js
index 5fd9bd92b..46abc3a45 100644
--- a/web/pgadmin/browser/static/js/constants.js
+++ b/web/pgadmin/browser/static/js/constants.js
@@ -1,3 +1,14 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2024, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from 'sources/gettext';
+
export const AUTH_METHODS = {
INTERNAL: 'internal',
LDAP: 'ldap',
@@ -32,3 +43,64 @@ export const BROWSER_PANELS = {
USER_MANAGEMENT: 'id-user-management',
IMPORT_EXPORT_SERVERS: 'id-import-export-servers'
};
+
+export const WEEKDAYS = [
+ {label: gettext('Sunday'), value: '7'},
+ {label: gettext('Monday'), value: '1'},
+ {label: gettext('Tuesday'), value: '2'},
+ {label: gettext('Wednesday'), value: '3'},
+ {label: gettext('Thursday'), value: '4'},
+ {label: gettext('Friday'), value: '5'},
+ {label: gettext('Saturday'), value: '6'},
+ ],
+ MONTHDAYS = [
+ {label: gettext('1st'), value: '1'}, {label: gettext('2nd'), value: '2'},
+ {label: gettext('3rd'), value: '3'}, {label: gettext('4th'), value: '4'},
+ {label: gettext('5th'), value: '5'}, {label: gettext('6th'), value: '6'},
+ {label: gettext('7th'), value: '7'}, {label: gettext('8th'), value: '8'},
+ {label: gettext('9th'), value: '9'}, {label: gettext('10th'), value: '10'},
+ {label: gettext('11th'), value: '11'}, {label: gettext('12th'), value: '12'},
+ {label: gettext('13th'), value: '13'}, {label: gettext('14th'), value: '14'},
+ {label: gettext('15th'), value: '15'}, {label: gettext('16th'), value: '16'},
+ {label: gettext('17th'), value: '17'}, {label: gettext('18th'), value: '18'},
+ {label: gettext('19th'), value: '19'}, {label: gettext('20th'), value: '20'},
+ {label: gettext('21st'), value: '21'}, {label: gettext('22nd'), value: '22'},
+ {label: gettext('23rd'), value: '23'}, {label: gettext('24th'), value: '24'},
+ {label: gettext('25th'), value: '25'}, {label: gettext('26th'), value: '26'},
+ {label: gettext('27th'), value: '27'}, {label: gettext('28th'), value: '28'},
+ {label: gettext('29th'), value: '29'}, {label: gettext('30th'), value: '30'},
+ {label: gettext('31st'), value: '31'},
+ ],
+ MONTHS = [
+ {label: gettext('January'),value: '1'}, {label: gettext('February'),value: '2'},
+ {label: gettext('March'), value: '3'}, {label: gettext('April'), value: '4'},
+ {label: gettext('May'), value: '5'}, {label: gettext('June'), value: '6'},
+ {label: gettext('July'), value: '7'}, {label: gettext('August'), value: '8'},
+ {label: gettext('September'), value: '9'}, {label: gettext('October'), value: '10'},
+ {label: gettext('November'), value: '11'}, {label: gettext('December'), value: '12'},
+ ],
+ HOURS = [
+ {label: gettext('00'), value: '00'}, {label: gettext('01'), value: '01'}, {label: gettext('02'), value: '02'}, {label: gettext('03'), value: '03'},
+ {label: gettext('04'), value: '04'}, {label: gettext('05'), value: '05'}, {label: gettext('06'), value: '06'}, {label: gettext('07'), value: '07'},
+ {label: gettext('08'), value: '08'}, {label: gettext('09'), value: '09'}, {label: gettext('10'), value: '10'}, {label: gettext('11'), value: '11'},
+ {label: gettext('12'), value: '12'}, {label: gettext('13'), value: '13'}, {label: gettext('14'), value: '14'}, {label: gettext('15'), value: '15'},
+ {label: gettext('16'), value: '16'}, {label: gettext('17'), value: '17'}, {label: gettext('18'), value: '18'}, {label: gettext('19'), value: '19'},
+ {label: gettext('20'), value: '20'}, {label: gettext('21'), value: '21'}, {label: gettext('22'), value: '22'}, {label: gettext('23'), value: '23'},
+ ],
+ MINUTES = [
+ {label: gettext('00'), value: '00'}, {label: gettext('01'), value: '01'}, {label: gettext('02'), value: '02'}, {label: gettext('03'), value: '03'},
+ {label: gettext('04'), value: '04'}, {label: gettext('05'), value: '05'}, {label: gettext('06'), value: '06'}, {label: gettext('07'), value: '07'},
+ {label: gettext('08'), value: '08'}, {label: gettext('09'), value: '09'}, {label: gettext('10'), value: '10'}, {label: gettext('11'), value: '11'},
+ {label: gettext('12'), value: '12'}, {label: gettext('13'), value: '13'}, {label: gettext('14'), value: '14'}, {label: gettext('15'), value: '15'},
+ {label: gettext('16'), value: '16'}, {label: gettext('17'), value: '17'}, {label: gettext('18'), value: '18'}, {label: gettext('19'), value: '19'},
+ {label: gettext('20'), value: '20'}, {label: gettext('21'), value: '21'}, {label: gettext('22'), value: '22'}, {label: gettext('23'), value: '23'},
+ {label: gettext('24'), value: '24'}, {label: gettext('25'), value: '25'}, {label: gettext('26'), value: '26'}, {label: gettext('27'), value: '27'},
+ {label: gettext('28'), value: '28'}, {label: gettext('29'), value: '29'}, {label: gettext('30'), value: '30'}, {label: gettext('31'), value: '31'},
+ {label: gettext('32'), value: '32'}, {label: gettext('33'), value: '33'}, {label: gettext('34'), value: '34'}, {label: gettext('35'), value: '35'},
+ {label: gettext('36'), value: '36'}, {label: gettext('37'), value: '37'}, {label: gettext('38'), value: '38'}, {label: gettext('39'), value: '39'},
+ {label: gettext('40'), value: '40'}, {label: gettext('41'), value: '41'}, {label: gettext('42'), value: '42'}, {label: gettext('43'), value: '43'},
+ {label: gettext('44'), value: '44'}, {label: gettext('45'), value: '45'}, {label: gettext('46'), value: '46'}, {label: gettext('47'), value: '47'},
+ {label: gettext('48'), value: '48'}, {label: gettext('49'), value: '49'}, {label: gettext('50'), value: '50'}, {label: gettext('51'), value: '51'},
+ {label: gettext('52'), value: '52'}, {label: gettext('53'), value: '53'}, {label: gettext('54'), value: '54'}, {label: gettext('55'), value: '55'},
+ {label: gettext('56'), value: '56'}, {label: gettext('57'), value: '57'}, {label: gettext('58'), value: '58'}, {label: gettext('59'), value: '59'},
+ ];
diff --git a/web/pgadmin/help/static/js/help.js b/web/pgadmin/help/static/js/help.js
index 815f99f3a..d30eb952a 100644
--- a/web/pgadmin/help/static/js/help.js
+++ b/web/pgadmin/help/static/js/help.js
@@ -27,10 +27,10 @@ export function getHelpUrl(base_path, file, version) {
return url + file;
}
-export function getEPASHelpUrl(version) {
+export function getEPASHelpUrl(version, epasURL=null) {
let major = Math.floor(version / 10000),
minor = Math.floor(version / 100) - (major * 100),
- epasHelp11Plus = 'https://www.enterprisedb.com/docs/epas/$VERSION$/epas_compat_sql/',
+ epasHelp11Plus = epasURL??'https://www.enterprisedb.com/docs/epas/$VERSION$/epas_compat_sql/',
epasHelp = 'https://www.enterprisedb.com/docs/epas/$VERSION$/',
url = '';
diff --git a/web/pgadmin/misc/properties/CollectionNodeProperties.jsx b/web/pgadmin/misc/properties/CollectionNodeProperties.jsx
index a9daa8145..f8645735f 100644
--- a/web/pgadmin/misc/properties/CollectionNodeProperties.jsx
+++ b/web/pgadmin/misc/properties/CollectionNodeProperties.jsx
@@ -210,7 +210,7 @@ export default function CollectionNodeProperties({
setLoaderText(gettext('Loading...'));
- if (nodeData._type.indexOf('coll-') > -1 && !_.isUndefined(nodeObj.getSchema)) {
+ if (!_.isUndefined(nodeObj.getSchema)) {
schemaRef.current = nodeObj.getSchema?.call(nodeObj, treeNodeInfo, nodeData);
schemaRef.current?.fields.forEach((field) => {
if (node.columns.indexOf(field.id) > -1) {
diff --git a/web/pgadmin/misc/properties/ObjectNodeProperties.jsx b/web/pgadmin/misc/properties/ObjectNodeProperties.jsx
index 3035ea222..0a76b229b 100644
--- a/web/pgadmin/misc/properties/ObjectNodeProperties.jsx
+++ b/web/pgadmin/misc/properties/ObjectNodeProperties.jsx
@@ -146,7 +146,7 @@ export default function ObjectNodeProperties({panelId, node, treeNodeInfo, nodeD
let fullUrl = '';
if (server.server_type == 'ppas' && node.epasHelp) {
- fullUrl = getEPASHelpUrl(server.version);
+ fullUrl = getEPASHelpUrl(server.version, node.epasURL);
} else if (node.sqlCreateHelp == '' && node.sqlAlterHelp != '') {
fullUrl = getHelpUrl(helpUrl, node.sqlAlterHelp, server.version);
} else if (node.sqlCreateHelp != '' && node.sqlAlterHelp == '') {
diff --git a/web/pgadmin/misc/properties/Properties.jsx b/web/pgadmin/misc/properties/Properties.jsx
index 56924bd04..146761be6 100644
--- a/web/pgadmin/misc/properties/Properties.jsx
+++ b/web/pgadmin/misc/properties/Properties.jsx
@@ -1,3 +1,12 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2024, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
import React from 'react';
import CollectionNodeProperties from './CollectionNodeProperties';
import ErrorBoundary from '../../static/js/helpers/ErrorBoundary';
@@ -9,6 +18,7 @@ import gettext from 'sources/gettext';
import { Box, makeStyles } from '@material-ui/core';
import { usePgAdmin } from '../../static/js/BrowserComponent';
import PropTypes from 'prop-types';
+import _ from 'lodash';
const useStyles = makeStyles((theme) => ({
root: {
@@ -20,15 +30,22 @@ const useStyles = makeStyles((theme) => ({
}));
function Properties(props) {
- const isCollection = props.nodeData?._type?.startsWith('coll-');
+ const isCollection = props.nodeData?._type?.startsWith('coll-') || props.nodeData?._type == 'dbms_job_scheduler';
const classes = useStyles();
const pgAdmin = usePgAdmin();
+ let noPropertyMsg = '';
- if(!props.node) {
+ if (!props.node) {
+ noPropertyMsg = gettext('Please select an object in the tree view.');
+ } else if (!_.isUndefined(props.node.hasProperties) && !props.node.hasProperties) {
+ noPropertyMsg = gettext('No information is available for the selected object.');
+ }
+
+ if(noPropertyMsg) {
return (
-
+
);
diff --git a/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js b/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js
index daa8bf945..a56ef1b68 100644
--- a/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js
+++ b/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js
@@ -139,7 +139,7 @@ _.extend(pgBrowser.browserTreeState, {
update_cache: function(item) {
let data = item && pgBrowser.tree.itemData(item),
treeHierarchy = pgBrowser.tree.getTreeNodeHierarchy(item),
- topParent = undefined,
+ topParent,
pathIDs = pgBrowser.tree.pathId(pgBrowser.tree.parent(item)),
oldPath = pathIDs.join(),
path = [],
@@ -204,7 +204,7 @@ _.extend(pgBrowser.browserTreeState, {
this.update_database_status(item);
if (data._type == self.parent || data._type == 'database') {
- if (topParent in treeData && 'paths' in treeData[topParent]) {
+ if (treeData?.[topParent]?.['paths'] && self.current_state?.[topParent]?.['paths']) {
treeData[topParent]['paths'] = self.current_state[topParent]['paths'];
self.save_state();
}
@@ -213,14 +213,13 @@ _.extend(pgBrowser.browserTreeState, {
if (pgBrowser.tree.isClosed(item)) {
let tmpTreeData = self.current_state[topParent]['paths'],
- databaseId = undefined;
+ databaseId;
if (treeHierarchy.hasOwnProperty('database'))
databaseId = treeHierarchy['database']['id'];
if (!_.isUndefined(tmpTreeData) && !_.isUndefined(tmpTreeData.length)) {
- let tcnt = 0,
- tmpItemDataStr = undefined;
+ let tcnt = 0, tmpItemDataStr;
_.each(tmpTreeData, function(tData) {
if (_.isUndefined(tData))
return;
@@ -271,7 +270,7 @@ _.extend(pgBrowser.browserTreeState, {
if (!_.isUndefined(tmpTreeData) && ('paths' in tmpTreeData) && !_.isUndefined(tmpTreeData['paths'].length)) {
let tmpTreeDataPaths = [...tmpTreeData['paths']],
- databaseId = undefined;
+ databaseId;
if (treeHierarchy.hasOwnProperty('database'))
databaseId = treeHierarchy['database']['id'];
@@ -335,7 +334,7 @@ _.extend(pgBrowser.browserTreeState, {
let topParent = treeHierarchy?.[this.parent]['_id'],
selectedItem = pgBrowser.tree.itemData(pgBrowser.tree.selected()),
- databaseItem = undefined;
+ databaseItem;
selectedItem = selectedItem ? selectedItem.id : undefined;
diff --git a/web/pgadmin/utils/constants.py b/web/pgadmin/utils/constants.py
index 790bdae8c..9ea96fb03 100644
--- a/web/pgadmin/utils/constants.py
+++ b/web/pgadmin/utils/constants.py
@@ -137,3 +137,6 @@ class MessageType:
INFO = 'Info',
CLOSE = 'Close',
WARNING = 'Warning'
+
+
+DBMS_JOB_SCHEDULER_ID = 999999
diff --git a/web/regression/re_sql/tests/test_resql.py b/web/regression/re_sql/tests/test_resql.py
index b9b5d77e2..534953957 100644
--- a/web/regression/re_sql/tests/test_resql.py
+++ b/web/regression/re_sql/tests/test_resql.py
@@ -13,7 +13,6 @@ import traceback
from urllib.parse import urlencode
from flask import url_for
import regression
-import config
from regression import parent_node_dict
from pgadmin.utils.route import BaseTestGenerator
from regression.python_test_utils import test_utils as utils
@@ -21,7 +20,7 @@ from pgadmin.browser.server_groups.servers.databases.tests import \
utils as database_utils
from pgadmin.utils.versioned_template_loader import \
get_version_mapping_directories
-from config import PG_DEFAULT_DRIVER
+from pgadmin.utils.constants import DBMS_JOB_SCHEDULER_ID
def create_resql_module_list(all_modules, exclude_pkgs, for_modules):
@@ -213,6 +212,8 @@ class ReverseEngineeredSQLTestCases(BaseTestGenerator):
# fsid represents Foreign Server oid
elif arg == 'fsid' and 'fsid' in self.parent_ids:
options['fsid'] = int(self.parent_ids['fsid'])
+ elif arg == 'jsid':
+ options['jsid'] = DBMS_JOB_SCHEDULER_ID
else:
if object_id is not None:
try:
@@ -447,7 +448,8 @@ class ReverseEngineeredSQLTestCases(BaseTestGenerator):
# urlencode
msql_data = {
key: json.dumps(val)
- if isinstance(val, dict) or isinstance(val, list) else val
+ if isinstance(val, dict) or isinstance(val, list) else
+ (val if val is not None else 'null')
for key, val in scenario['data'].items()}
params = urlencode(msql_data)
diff --git a/web/webpack.config.js b/web/webpack.config.js
index b02958fa2..265732808 100644
--- a/web/webpack.config.js
+++ b/web/webpack.config.js
@@ -502,6 +502,7 @@ module.exports = [{
'pure|pgadmin.node.compound_trigger',
'pure|pgadmin.node.aggregate',
'pure|pgadmin.node.operator',
+ 'pure|pgadmin.node.dbms_job_scheduler',
],
},
},
diff --git a/web/webpack.shim.js b/web/webpack.shim.js
index 07da0929c..b3035f4ef 100644
--- a/web/webpack.shim.js
+++ b/web/webpack.shim.js
@@ -86,6 +86,10 @@ let webpackShimConfig = {
'pgadmin.node.compound_trigger': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/static/js/compound_trigger'),
'pgadmin.node.constraints': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints'),
'pgadmin.node.database': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/static/js/database'),
+ 'pgadmin.node.dbms_job_scheduler': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_job_scheduler'),
+ 'pgadmin.node.dbms_job': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/static/js/dbms_job'),
+ 'pgadmin.node.dbms_program': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/static/js/dbms_program'),
+ 'pgadmin.node.dbms_schedule': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/static/js/dbms_schedule'),
'pgadmin.node.domain': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain'),
'pgadmin.node.domain_constraints': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/domains/domain_constraints/static/js/domain_constraints'),
'pgadmin.node.event_trigger': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/event_triggers/static/js/event_trigger'),