diff --git a/docs/en_US/backup_dialog.rst b/docs/en_US/backup_dialog.rst index 02cc46ba788..ecbffc68cc8 100644 --- a/docs/en_US/backup_dialog.rst +++ b/docs/en_US/backup_dialog.rst @@ -100,6 +100,10 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg * Move the switch next to *Only schemas* to limit the back up to schema-level database objects. + * Move the switch next to *Only statistics* to the *Yes* position to limit + the back up to optimizer statistics. **Note:** This option is visible only + for database server greater than or equal to 18. + * Move the switch next to *Blobs* to the *No* position to exclude large objects in the backup. @@ -143,6 +147,22 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg Table access methods. **Note:** This option is visible only for database server greater than or equal to 15. + * Move the switch next to *Row security policies* to the *Yes* position to + exclude row security policies. **Note:** This option is visible only for + database server greater than or equal to 18. + + * Move the switch next to *Data* to the *Yes* position to exclude data from + the backup. **Note:** This option is visible only for database server + greater than or equal to 18. + + * Move the switch next to *Schema* to the *Yes* position to exclude schema + (the data definitions) from the backup. **Note:** This option is visible + only for database server greater than or equal to 18. + + * Move the switch next to *Statistics* to the *Yes* position to exclude + optimizer statistics. **Note:** This option is visible only for database + server greater than or equal to 18. + .. image:: images/backup_queries.png :alt: Queries option on backup dialog :align: center @@ -259,6 +279,14 @@ tab to provide other backup options. position to include a statement that will use a SET SESSION AUTHORIZATION command to determine object ownership (instead of an ALTER OWNER command). + * Move the switch next to *Dump statistics* to the *Yes* position to include + optimizer statistics in the backup. **Note:** This option is visible only + for database server greater than or equal to 18. + + * Move the switch next to *Sequence data* to the *Yes* position to include + sequence data that would normally be excluded. **Note:** This option is + visible only for database server greater than or equal to 18. + * Use the *Exclude schema* field to not dump schemas whose name matches pattern. diff --git a/docs/en_US/backup_server_dialog.rst b/docs/en_US/backup_server_dialog.rst index ed9da9a5e1c..08def536906 100644 --- a/docs/en_US/backup_server_dialog.rst +++ b/docs/en_US/backup_server_dialog.rst @@ -45,6 +45,10 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg * Move the switch next to *Only roles* to limit the back up to roles only. + * Move the switch next to *Only statistics* to the *Yes* position to limit + the back up to optimizer statistics. **Note:** This option is visible only + for database server greater than or equal to 18. + .. image:: images/backup_server_do_not_save.png :alt: Do not save option on backup server dialog :align: center @@ -88,6 +92,22 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg Table access methods. **Note:** This option is visible only for database server greater than or equal to 15. + * Move the switch next to *Row security policies* to the *Yes* position to + exclude row security policies. **Note:** This option is visible only for + database server greater than or equal to 18. + + * Move the switch next to *Data* to the *Yes* position to exclude data from + the backup. **Note:** This option is visible only for database server + greater than or equal to 18. + + * Move the switch next to *Schema* to the *Yes* position to exclude schema + (the data definitions) from the backup. **Note:** This option is visible + only for database server greater than or equal to 18. + + * Move the switch next to *Statistics* to the *Yes* position to exclude + optimizer statistics. **Note:** This option is visible only for database + server greater than or equal to 18. + .. image:: images/backup_server_queries.png :alt: Queries option on backup server dialog :align: center @@ -171,6 +191,14 @@ tab to provide other backup options. position to include a statement that will use a SET SESSION AUTHORIZATION command to determine object ownership (instead of an ALTER OWNER command). + * Move the switch next to *Dump statistics* to the *Yes* position to include + optimizer statistics in the backup. **Note:** This option is visible only + for database server greater than or equal to 18. + + * Move the switch next to *Sequence data* to the *Yes* position to include + sequence data that would normally be excluded. **Note:** This option is + visible only for database server greater than or equal to 18. + * Use the *Exclude database* field to not dump databases whose name matches pattern. diff --git a/docs/en_US/restore_dialog.rst b/docs/en_US/restore_dialog.rst index 72d45d48ba2..9b0a657faad 100644 --- a/docs/en_US/restore_dialog.rst +++ b/docs/en_US/restore_dialog.rst @@ -77,6 +77,10 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg * Move the switch next to *Only schema* to limit the restoration to schema-level database objects. + * Move the switch next to *Only statistics* to the *Yes* position to limit + the restoration to optimizer statistics. **Note:** This option is visible + only for database server greater than or equal to 18. + .. image:: images/restore_do_not_save.png :alt: Restore dialog do not save section :align: center @@ -110,6 +114,22 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg Table access methods. **Note:** This option is visible only for database server greater than or equal to 15. + * Move the switch next to *Row security policies* to the *Yes* position to + exclude row security policies. **Note:** This option is visible only for + database server greater than or equal to 18. + + * Move the switch next to *Data* to the *Yes* position to exclude data from + the restore. **Note:** This option is visible only for database server + greater than or equal to 18. + + * Move the switch next to *Schema* to the *Yes* position to exclude schema + (the data definitions) from the restore. **Note:** This option is visible + only for database server greater than or equal to 18. + + * Move the switch next to *Statistics* to the *Yes* position to exclude + optimizer statistics from the restore. **Note:** This option is visible + only for database server greater than or equal to 18. + .. image:: images/restore_queries.png :alt: Restore dialog queries section :align: center diff --git a/web/pgadmin/tools/backup/__init__.py b/web/pgadmin/tools/backup/__init__.py index d54053d6f32..8d779273ef4 100644 --- a/web/pgadmin/tools/backup/__init__.py +++ b/web/pgadmin/tools/backup/__init__.py @@ -294,6 +294,14 @@ def set_value(key, param, default_value=None, assertion=True): data.get('only_tablespaces', None)) set_param('only_roles', '--roles-only', data.get('only_roles', None)) + # pg_dump rejects --statistics-only alongside --data-only or + # --schema-only, so suppress it the same way --schema-only is suppressed + # above. The dialog disables the switches, but a request can still arrive + # with both set. + set_param('only_statistics', '--statistics-only', + manager.version >= 180000 and + not data.get('only_data', None) and + not data.get('only_schema', None)) # Sections set_param('pre_data', '--section=pre-data') @@ -317,6 +325,17 @@ def set_value(key, param, default_value=None, assertion=True): set_param('dns_table_access_method', '--no-table-access-method', manager.version >= 150000) set_param('dns_no_role_passwords', '--no-role-passwords') + set_param('no_policies', '--no-policies', manager.version >= 180000) + # Each of these conflicts with the matching --*-only option. + set_param('no_data', '--no-data', + manager.version >= 180000 and + not data.get('only_data', None)) + set_param('no_schema', '--no-schema', + manager.version >= 180000 and + not data.get('only_schema', None)) + set_param('no_statistics', '--no-statistics', + manager.version >= 180000 and + not data.get('only_statistics', None)) # Query Options set_param('use_insert_commands', '--inserts') @@ -353,6 +372,8 @@ def set_value(key, param, default_value=None, assertion=True): set_param('verbose', '--verbose') set_param('dqoute', '--quote-all-identifiers') set_param('use_set_session_auth', '--use-set-session-authorization') + set_param('statistics', '--statistics', manager.version >= 180000) + set_param('sequence_data', '--sequence-data', manager.version >= 180000) set_value('exclude_schema', '--exclude-schema') set_value('extra_float_digits', '--extra-float-digits', None, manager.version >= 120000) diff --git a/web/pgadmin/tools/backup/static/js/backup.ui.js b/web/pgadmin/tools/backup/static/js/backup.ui.js index 9460b5b6a82..5b6bc9bc05c 100644 --- a/web/pgadmin/tools/backup/static/js/backup.ui.js +++ b/web/pgadmin/tools/backup/static/js/backup.ui.js @@ -98,14 +98,15 @@ export class TypeObjSchema extends BaseUISchema { type: 'switch', group: gettext('Type of objects'), deps: ['pre_data', 'data', 'post_data', 'only_schema', - 'only_tablespaces', 'only_roles'], + 'only_tablespaces', 'only_roles', 'only_statistics'], disabled: function(state) { return state.pre_data || state.data || state.post_data || state.only_schema || state.only_tablespaces || - state.only_roles; + state.only_roles || + state.only_statistics; }, inlineGroup: 'type_of_objects', }, { @@ -114,14 +115,15 @@ export class TypeObjSchema extends BaseUISchema { type: 'switch', group: gettext('Type of objects'), deps: ['pre_data', 'data', 'post_data', 'only_data', - 'only_tablespaces', 'only_roles'], + 'only_tablespaces', 'only_roles', 'only_statistics'], disabled: function(state) { return state.pre_data || state.data || state.post_data || state.only_data || state.only_tablespaces || - state.only_roles; + state.only_roles || + state.only_statistics; }, inlineGroup: 'type_of_objects', }, { @@ -130,14 +132,15 @@ export class TypeObjSchema extends BaseUISchema { type: 'switch', group: gettext('Type of objects'), deps: ['pre_data', 'data', 'post_data', 'only_data', 'only_schema', - 'only_roles'], + 'only_roles', 'only_statistics'], disabled: function(state) { return state.pre_data || state.data || state.post_data || state.only_data || state.only_schema || - state.only_roles; + state.only_roles || + state.only_statistics; }, visible: isVisibleForObjectBackup(obj?.top?.backupType), inlineGroup: 'type_of_objects', @@ -147,7 +150,7 @@ export class TypeObjSchema extends BaseUISchema { type: 'switch', group: gettext('Type of objects'), deps: ['pre_data', 'data', 'post_data', 'only_data', 'only_schema', - 'only_tablespaces'], + 'only_tablespaces', 'only_statistics'], inlineGroup: 'type_of_objects', disabled: function(state) { return state.pre_data || @@ -155,9 +158,28 @@ export class TypeObjSchema extends BaseUISchema { state.post_data || state.only_data || state.only_schema || - state.only_tablespaces; + state.only_tablespaces || + state.only_statistics; }, visible: isVisibleForObjectBackup(obj?.top?.backupType) + }, { + id: 'only_statistics', + label: gettext('Only statistics'), + type: 'switch', + group: gettext('Type of objects'), + min_version: 180000, + deps: ['pre_data', 'data', 'post_data', 'only_data', 'only_schema', + 'only_tablespaces', 'only_roles'], + inlineGroup: 'type_of_objects', + disabled: function(state) { + return state.pre_data || + state.data || + state.post_data || + state.only_data || + state.only_schema || + state.only_tablespaces || + state.only_roles; + }, }, { id: 'blobs', label: gettext('Blobs'), @@ -282,6 +304,49 @@ export class SaveOptSchema extends BaseUISchema { group: gettext('Do not save'), inlineGroup: 'do_not_save', min_version: 150000 + }, { + id: 'no_policies', + label: gettext('Row security policies'), + type: 'switch', + disabled: false, + group: gettext('Do not save'), + inlineGroup: 'do_not_save', + min_version: 180000 + }, { + // pg_dump rejects each of these alongside the matching *Only* switch + // on the Data/Objects tab, so they follow each other's state. + id: 'no_data', + label: gettext('Data'), + type: 'switch', + group: gettext('Do not save'), + inlineGroup: 'do_not_save', + min_version: 180000, + deps: ['only_data'], + disabled: function(state) { + return state.only_data; + }, + }, { + id: 'no_schema', + label: gettext('Schema'), + type: 'switch', + group: gettext('Do not save'), + inlineGroup: 'do_not_save', + min_version: 180000, + deps: ['only_schema'], + disabled: function(state) { + return state.only_schema; + }, + }, { + id: 'no_statistics', + label: gettext('Statistics'), + type: 'switch', + group: gettext('Do not save'), + inlineGroup: 'do_not_save', + min_version: 180000, + deps: ['only_statistics'], + disabled: function(state) { + return state.only_statistics; + }, }]; } } @@ -384,6 +449,22 @@ export class MiscellaneousSchema extends BaseUISchema { disabled: false, group: gettext('Miscellaneous'), inlineGroup: 'miscellaneous', + }, { + id: 'statistics', + label: gettext('Dump statistics'), + type: 'switch', + disabled: false, + group: gettext('Miscellaneous'), + inlineGroup: 'miscellaneous', + min_version: 180000 + }, { + id: 'sequence_data', + label: gettext('Sequence data'), + type: 'switch', + disabled: false, + group: gettext('Miscellaneous'), + inlineGroup: 'miscellaneous', + min_version: 180000 }, { id: 'exclude_schema', label: gettext('Exclude schema'), diff --git a/web/pgadmin/tools/backup/tests/test_backup_create_job_unit_test.py b/web/pgadmin/tools/backup/tests/test_backup_create_job_unit_test.py index 6e613f17970..c7998967f44 100644 --- a/web/pgadmin/tools/backup/tests/test_backup_create_job_unit_test.py +++ b/web/pgadmin/tools/backup/tests/test_backup_create_job_unit_test.py @@ -1301,6 +1301,134 @@ class BackupCreateJobTest(BaseTestGenerator): expected_cmd_opts=['--globals-only'], not_expected_cmd_opts=[], expected_exit_code=[0, None] + )), + ('When backup the object with option - Do not save Row security ' + 'policies, Data, Schema (>= v18)', + dict( + class_params=dict( + sid=1, + name='test_backup_server', + port=5444, + host='localhost', + database='postgres', + bfile='test_backup', + username='postgres' + ), + params=dict( + file='test_backup_file', + format='custom', + verbose=True, + schemas=[], + tables=[], + database='postgres', + no_policies=True, + no_data=True, + no_schema=True, + ), + url=BACKUP_OBJECT_URL, + expected_cmd_opts=['--no-policies', '--no-data', '--no-schema'], + not_expected_cmd_opts=[], + expected_exit_code=[0, None], + server_min_version=180000, + message='Backup object with --no-policies, --no-data, ' + '--no-schema is not supported by EPAS/PG server less ' + 'than 18.0' + )), + ('When backup the object with option - statistics, sequence data ' + 'and only statistics (>= v18)', + dict( + class_params=dict( + sid=1, + name='test_backup_server', + port=5444, + host='localhost', + database='postgres', + bfile='test_backup', + username='postgres' + ), + params=dict( + file='test_backup_file', + format='plain', + verbose=True, + schemas=[], + tables=[], + database='postgres', + only_statistics=True, + statistics=True, + sequence_data=True, + ), + url=BACKUP_OBJECT_URL, + expected_cmd_opts=['--statistics-only', '--statistics', + '--sequence-data'], + not_expected_cmd_opts=[], + expected_exit_code=[0, None], + server_min_version=180000, + message='Backup object with --statistics-only, --statistics, ' + '--sequence-data is not supported by EPAS/PG server less ' + 'than 18.0' + )), + ('When backup the object with v18 options against an older server ' + '(< v18)', + dict( + class_params=dict( + sid=1, + name='test_backup_server', + port=5444, + host='localhost', + database='postgres', + bfile='test_backup', + username='postgres' + ), + params=dict( + file='test_backup_file', + format='custom', + verbose=True, + schemas=[], + tables=[], + database='postgres', + no_policies=True, + no_data=True, + no_schema=True, + only_statistics=True, + statistics=True, + sequence_data=True, + ), + url=BACKUP_OBJECT_URL, + expected_cmd_opts=[], + not_expected_cmd_opts=['--no-policies', '--no-data', + '--no-schema', '--statistics-only', + '--statistics', '--sequence-data'], + expected_exit_code=[0, None], + server_max_version=179999, + message='v18 backup options must not be emitted for EPAS/PG ' + 'server less than 18.0' + )), + ('When backup the server with v18 options (>= v18)', + dict( + class_params=dict( + sid=1, + name='test_backup_server', + port=5444, + host='localhost', + database='postgres', + bfile='test_backup', + username='postgres' + ), + params=dict( + file='test_backup_server_file', + type='server', + format='plain', + verbose=True, + no_policies=True, + sequence_data=True, + ), + url=BACKUP_SERVER_URL, + expected_cmd_opts=['--no-policies', '--sequence-data'], + not_expected_cmd_opts=[], + expected_exit_code=[0, None], + server_min_version=180000, + message='Backup server with --no-policies, --sequence-data is ' + 'not supported by EPAS/PG server less than 18.0' )) ] diff --git a/web/pgadmin/tools/backup/tests/test_backup_pg18_args_unit_test.py b/web/pgadmin/tools/backup/tests/test_backup_pg18_args_unit_test.py new file mode 100644 index 00000000000..8fd05d6dc72 --- /dev/null +++ b/web/pgadmin/tools/backup/tests/test_backup_pg18_args_unit_test.py @@ -0,0 +1,113 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2026, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Unit tests for the PostgreSQL 18 pg_dump options. + +These call _get_args_params_values() with a mocked manager rather than going +through the API against a live server, so the version-gated behaviour is +covered whatever the test server happens to be. The existing scenarios in +test_backup_create_job_unit_test.py can only assert the v18 options where a +v18 server is available to test against, and silently skip otherwise. + +Each of --statistics-only, --no-data, --no-schema and --no-statistics +conflicts with one of the --*-only options, verified against pg_dump 18: +"options -s/--schema-only and --statistics-only cannot be used together" and +so on. The utility rejects the whole command, so pgAdmin must not emit both +even if a request arrives with both set. +""" + +from unittest.mock import MagicMock, patch + +from pgadmin.tools.backup import _get_args_params_values +from pgadmin.utils.route import BaseTestGenerator + +V18 = 180000 +V17 = 170000 + +ALL_V18_OPTS = ['--statistics-only', '--no-policies', '--no-data', + '--no-schema', '--no-statistics', '--statistics', + '--sequence-data'] + + +class BackupPG18ArgsTestCase(BaseTestGenerator): + """The v18 options must be emitted only when they are usable.""" + + scenarios = [ + ('Do not save options are emitted on v18', dict( + version=V18, + data=dict(no_policies=True, no_data=True, no_schema=True, + no_statistics=True), + expected=['--no-policies', '--no-data', '--no-schema', + '--no-statistics'], + not_expected=['--statistics-only'], + )), + ('Statistics and sequence data are emitted on v18', dict( + version=V18, + data=dict(statistics=True, sequence_data=True), + expected=['--statistics', '--sequence-data'], + not_expected=['--statistics-only'], + )), + ('Only statistics is emitted on v18', dict( + version=V18, + data=dict(only_statistics=True), + expected=['--statistics-only'], + not_expected=['--data-only', '--schema-only'], + )), + ('No v18 option is emitted on v17', dict( + version=V17, + data=dict(no_policies=True, no_data=True, no_schema=True, + no_statistics=True, only_statistics=True, + statistics=True, sequence_data=True), + expected=[], + not_expected=ALL_V18_OPTS, + )), + ('Only data wins over only statistics and no data', dict( + version=V18, + data=dict(only_data=True, only_statistics=True, no_data=True), + expected=['--data-only'], + not_expected=['--statistics-only', '--no-data'], + )), + ('Only schema wins over only statistics and no schema', dict( + version=V18, + data=dict(only_schema=True, only_statistics=True, no_schema=True), + expected=['--schema-only'], + not_expected=['--statistics-only', '--no-schema'], + )), + ('Only statistics wins over no statistics', dict( + version=V18, + data=dict(only_statistics=True, no_statistics=True), + expected=['--statistics-only'], + not_expected=['--no-statistics'], + )), + ] + + def setUp(self): + # A pure argument-marshalling test: no server connection needed. + pass + + def _args(self): + manager = MagicMock(version=self.version, user='postgres', + use_ssh_tunnel=0) + server = MagicMock(host='localhost', port=5432, + maintenance_db='postgres') + data = dict(format='custom', schemas=[], tables=[], **self.data) + + with patch('pgadmin.utils.driver.get_driver', + return_value=MagicMock()): + return _get_args_params_values( + data, MagicMock(), 'objects', 'test_backup_file', server, + manager) + + def runTest(self): + args = self._args() + + for opt in self.expected: + self.assertIn(opt, args) + for opt in self.not_expected: + self.assertNotIn(opt, args) diff --git a/web/pgadmin/tools/restore/__init__.py b/web/pgadmin/tools/restore/__init__.py index e65c3ec84dd..7b80b676f1f 100644 --- a/web/pgadmin/tools/restore/__init__.py +++ b/web/pgadmin/tools/restore/__init__.py @@ -360,6 +360,25 @@ def get_restore_util_args(data, manager, server, driver, conn, filepath): set_param('exit_on_error', '--exit-on-error', data, args) set_value('exclude_schema', '--exclude-schema', data, args) + # PostgreSQL 18 and above options + if manager.version >= 180000: + # pg_restore refuses --statistics-only alongside --data-only or + # --schema-only, and each --no-* option below alongside the + # matching --*-only, so drop the narrower option rather than + # letting the utility reject the whole command. The dialog + # disables these switches, but a request can still arrive with + # both set. + if not data.get('only_data', None) and \ + not data.get('only_schema', None): + set_param('only_statistics', '--statistics-only', data, args) + set_param('no_policies', '--no-policies', data, args) + if not data.get('only_data', None): + set_param('no_data', '--no-data', data, args) + if not data.get('only_schema', None): + set_param('no_schema', '--no-schema', data, args) + if not data.get('only_statistics', None): + set_param('no_statistics', '--no-statistics', data, args) + set_multiple('schemas', '--schema', data, args, driver, conn, False) set_multiple('tables', '--table', data, args, driver, conn, False) set_multiple('functions', '--function', data, args, driver, conn, diff --git a/web/pgadmin/tools/restore/static/js/restore.ui.js b/web/pgadmin/tools/restore/static/js/restore.ui.js index 34066a35db1..5d0d2dd0ca4 100644 --- a/web/pgadmin/tools/restore/static/js/restore.ui.js +++ b/web/pgadmin/tools/restore/static/js/restore.ui.js @@ -99,7 +99,7 @@ export class RestoreTypeObjSchema extends BaseUISchema { type: 'switch', group: gettext('Type of objects'), inlineGroup: 'types_of_data', - deps: ['pre_data', 'data', 'post_data', 'only_schema'], + deps: ['pre_data', 'data', 'post_data', 'only_schema', 'only_statistics'], disabled: function(state) { if(obj.selectedNodeType == 'table') { state.only_data = true; @@ -108,7 +108,8 @@ export class RestoreTypeObjSchema extends BaseUISchema { (state.pre_data || state.data || state.post_data || - state.only_schema + state.only_schema || + state.only_statistics ); }, }, { @@ -117,7 +118,7 @@ export class RestoreTypeObjSchema extends BaseUISchema { type: 'switch', group: gettext('Type of objects'), inlineGroup: 'types_of_data', - deps: ['pre_data', 'data', 'post_data', 'only_data'], + deps: ['pre_data', 'data', 'post_data', 'only_data', 'only_statistics'], disabled: function(state) { if(obj.selectedNodeType == 'index' || obj.selectedNodeType == 'function') { state.only_schema = true; @@ -126,7 +127,25 @@ export class RestoreTypeObjSchema extends BaseUISchema { (state.pre_data || state.data || state.post_data || - state.only_data + state.only_data || + state.only_statistics + ); + }, + }, { + id: 'only_statistics', + label: gettext('Only statistics'), + type: 'switch', + group: gettext('Type of objects'), + inlineGroup: 'types_of_data', + min_version: 180000, + deps: ['pre_data', 'data', 'post_data', 'only_data', 'only_schema'], + disabled: function(state) { + return (obj.selectedNodeType !== 'database' && obj.selectedNodeType !== 'schema') || + (state.pre_data || + state.data || + state.post_data || + state.only_data || + state.only_schema ); }, }]; @@ -217,6 +236,49 @@ export class RestoreSaveOptSchema extends BaseUISchema { group: gettext('Do not save'), inlineGroup: 'save_options', min_version: 150000 + }, { + id: 'no_policies', + label: gettext('Row security policies'), + type: 'switch', + disabled: false, + group: gettext('Do not save'), + inlineGroup: 'save_options', + min_version: 180000 + }, { + // pg_restore rejects each of these alongside the matching *Only* + // switch on the Data/Objects tab, so they follow each other's state. + id: 'no_data', + label: gettext('Data'), + type: 'switch', + group: gettext('Do not save'), + inlineGroup: 'save_options', + min_version: 180000, + deps: ['only_data'], + disabled: function(state) { + return state.only_data; + }, + }, { + id: 'no_schema', + label: gettext('Schema'), + type: 'switch', + group: gettext('Do not save'), + inlineGroup: 'save_options', + min_version: 180000, + deps: ['only_schema'], + disabled: function(state) { + return state.only_schema; + }, + }, { + id: 'no_statistics', + label: gettext('Statistics'), + type: 'switch', + group: gettext('Do not save'), + inlineGroup: 'save_options', + min_version: 180000, + deps: ['only_statistics'], + disabled: function(state) { + return state.only_statistics; + }, }]; } } diff --git a/web/pgadmin/tools/restore/tests/test_restore_create_job_unit_test.py b/web/pgadmin/tools/restore/tests/test_restore_create_job_unit_test.py index 8df960e7be4..2beb0759b90 100644 --- a/web/pgadmin/tools/restore/tests/test_restore_create_job_unit_test.py +++ b/web/pgadmin/tools/restore/tests/test_restore_create_job_unit_test.py @@ -408,6 +408,73 @@ class RestoreCreateJobTest(BaseTestGenerator): not_expected_cmd_opts=[], expected_exit_code=[0, None] )), + ('When restore object with option - Do not restore Row security ' + 'policies, Data, Schema, Statistics and Only statistics (>= v18)', + dict( + class_params=dict( + sid=1, + name='test_restore_server', + port=5444, + host='localhost', + database='postgres', + bfile='test_restore', + username='postgres' + ), + params=dict( + file='test_restore_file', + format='custom', + verbose=True, + schemas=[], + tables=[], + database='postgres', + no_policies=True, + no_data=True, + no_schema=True, + no_statistics=True, + ), + url=RESTORE_JOB_URL, + expected_cmd='pg_restore', + expected_cmd_opts=['--no-policies', '--no-data', '--no-schema', + '--no-statistics'], + not_expected_cmd_opts=['--statistics-only'], + expected_exit_code=[0, None], + server_min_version=180000, + message='Restore object with --no-policies, --no-data, ' + '--no-schema, --no-statistics is not ' + 'supported by EPAS/PG server less than 18.0' + )), + # Separate from the scenario above because pg_restore rejects + # --statistics-only alongside --no-statistics. + ('When restore object with option - Only statistics (>= v18)', + dict( + class_params=dict( + sid=1, + name='test_restore_server', + port=5444, + host='localhost', + database='postgres', + bfile='test_restore', + username='postgres' + ), + params=dict( + file='test_restore_file', + format='custom', + verbose=True, + schemas=[], + tables=[], + database='postgres', + only_statistics=True, + ), + url=RESTORE_JOB_URL, + expected_cmd='pg_restore', + expected_cmd_opts=['--statistics-only'], + not_expected_cmd_opts=['--no-statistics', '--data-only', + '--schema-only'], + expected_exit_code=[0, None], + server_min_version=180000, + message='Restore object with --statistics-only is not ' + 'supported by EPAS/PG server less than 18.0' + )), ] def setUp(self): diff --git a/web/pgadmin/tools/restore/tests/test_restore_pg18_args_unit_test.py b/web/pgadmin/tools/restore/tests/test_restore_pg18_args_unit_test.py new file mode 100644 index 00000000000..1917a50ae23 --- /dev/null +++ b/web/pgadmin/tools/restore/tests/test_restore_pg18_args_unit_test.py @@ -0,0 +1,97 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2026, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Unit tests for the PostgreSQL 18 pg_restore options. + +These call get_restore_util_args() with a mocked manager rather than going +through the API against a live server, so the version-gated behaviour is +covered whatever the test server happens to be. + +pg_restore 18 rejects --statistics-only alongside -a/--data-only or +-s/--schema-only, --no-data alongside --data-only, --no-schema alongside +--schema-only and --no-statistics alongside --statistics-only, so pgAdmin +must not emit both halves of any of those pairs. +""" + +from unittest.mock import MagicMock + +from pgadmin.tools.restore import get_restore_util_args +from pgadmin.utils.route import BaseTestGenerator + +V18 = 180000 +V17 = 170000 + +ALL_V18_OPTS = ['--statistics-only', '--no-policies', '--no-data', + '--no-schema', '--no-statistics'] + + +class RestorePG18ArgsTestCase(BaseTestGenerator): + """The v18 options must be emitted only when they are usable.""" + + scenarios = [ + ('Do not restore options are emitted on v18', dict( + version=V18, + data=dict(no_policies=True, no_data=True, no_schema=True, + no_statistics=True), + expected=['--no-policies', '--no-data', '--no-schema', + '--no-statistics'], + not_expected=['--statistics-only'], + )), + ('Only statistics is emitted on v18', dict( + version=V18, + data=dict(only_statistics=True), + expected=['--statistics-only'], + not_expected=['--data-only', '--schema-only'], + )), + ('No v18 option is emitted on v17', dict( + version=V17, + data=dict(no_policies=True, no_data=True, no_schema=True, + no_statistics=True, only_statistics=True), + expected=[], + not_expected=ALL_V18_OPTS, + )), + ('Only data wins over only statistics and no data', dict( + version=V18, + data=dict(only_data=True, only_statistics=True, no_data=True), + expected=['--data-only'], + not_expected=['--statistics-only', '--no-data'], + )), + ('Only schema wins over only statistics and no schema', dict( + version=V18, + data=dict(only_schema=True, only_statistics=True, no_schema=True), + expected=['--schema-only'], + not_expected=['--statistics-only', '--no-schema'], + )), + ('Only statistics wins over no statistics', dict( + version=V18, + data=dict(only_statistics=True, no_statistics=True), + expected=['--statistics-only'], + not_expected=['--no-statistics'], + )), + ] + + def setUp(self): + # A pure argument-marshalling test: no server connection needed. + pass + + def _args(self): + manager = MagicMock(version=self.version, use_ssh_tunnel=0) + server = MagicMock(host='localhost', port=5432, username='postgres') + data = dict(format='custom', **self.data) + + return get_restore_util_args(data, manager, server, MagicMock(), + MagicMock(), 'test_restore_file') + + def runTest(self): + args = self._args() + + for opt in self.expected: + self.assertIn(opt, args) + for opt in self.not_expected: + self.assertNotIn(opt, args)