From 1e44b2a62effccc69c10308826ec5f749150656e Mon Sep 17 00:00:00 2001 From: Dave Page Date: Wed, 10 Jun 2026 10:45:06 +0100 Subject: [PATCH 1/2] Add PostgreSQL 18 options to the Backup and Restore dialogs. Adds the new pg_dump/pg_dumpall/pg_restore options introduced in PostgreSQL 18, gated to servers >= 18: - Only statistics (--statistics-only) in Type of objects, mutually exclusive with the other Only * switches. - Row security policies (--no-policies), Data (--no-data) and Schema (--no-schema) in Do not save (both dialogs); Statistics (--no-statistics) in the Restore dialog. - Dump statistics (--statistics) and Sequence data (--sequence-data) in Backup Miscellaneous. The pg_dumpall -F/--format option and the pg_restore --exclude-database / -g/--globals-only options are not included: the non-text pg_dumpall feature was reverted before PostgreSQL 18.0 and those options do not exist in the shipped 18.x client utilities. Includes backend arg-builder wiring, unit-test scenarios and docs. Closes #9064 Closes #9065 Closes #9066 --- docs/en_US/backup_dialog.rst | 24 ++++ docs/en_US/backup_server_dialog.rst | 24 ++++ docs/en_US/restore_dialog.rst | 20 +++ web/pgadmin/tools/backup/__init__.py | 7 + .../tools/backup/static/js/backup.ui.js | 78 +++++++++-- .../tests/test_backup_create_job_unit_test.py | 128 ++++++++++++++++++ web/pgadmin/tools/restore/__init__.py | 8 ++ .../tools/restore/static/js/restore.ui.js | 59 +++++++- .../test_restore_create_job_unit_test.py | 36 +++++ 9 files changed, 372 insertions(+), 12 deletions(-) diff --git a/docs/en_US/backup_dialog.rst b/docs/en_US/backup_dialog.rst index 02cc46ba788..dd7cf0ea0cb 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,18 @@ 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. + .. image:: images/backup_queries.png :alt: Queries option on backup dialog :align: center @@ -259,6 +275,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..0d088fcbdb2 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,18 @@ 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. + .. image:: images/backup_server_queries.png :alt: Queries option on backup server dialog :align: center @@ -171,6 +187,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..5e6291650bd 100644 --- a/web/pgadmin/tools/backup/__init__.py +++ b/web/pgadmin/tools/backup/__init__.py @@ -294,6 +294,8 @@ 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)) + set_param('only_statistics', '--statistics-only', + manager.version >= 180000) # Sections set_param('pre_data', '--section=pre-data') @@ -317,6 +319,9 @@ 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) + set_param('no_data', '--no-data', manager.version >= 180000) + set_param('no_schema', '--no-schema', manager.version >= 180000) # Query Options set_param('use_insert_commands', '--inserts') @@ -353,6 +358,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..061611c306a 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,30 @@ 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 + }, { + id: 'no_data', + label: gettext('Data'), + type: 'switch', + disabled: false, + group: gettext('Do not save'), + inlineGroup: 'do_not_save', + min_version: 180000 + }, { + id: 'no_schema', + label: gettext('Schema'), + type: 'switch', + disabled: false, + group: gettext('Do not save'), + inlineGroup: 'do_not_save', + min_version: 180000 }]; } } @@ -384,6 +430,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/restore/__init__.py b/web/pgadmin/tools/restore/__init__.py index e65c3ec84dd..44e632958a1 100644 --- a/web/pgadmin/tools/restore/__init__.py +++ b/web/pgadmin/tools/restore/__init__.py @@ -360,6 +360,14 @@ 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: + set_param('only_statistics', '--statistics-only', data, args) + set_param('no_policies', '--no-policies', data, args) + set_param('no_data', '--no-data', data, args) + set_param('no_schema', '--no-schema', data, args) + 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..4c6cee3c0fa 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,38 @@ 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 + }, { + id: 'no_data', + label: gettext('Data'), + type: 'switch', + disabled: false, + group: gettext('Do not save'), + inlineGroup: 'save_options', + min_version: 180000 + }, { + id: 'no_schema', + label: gettext('Schema'), + type: 'switch', + disabled: false, + group: gettext('Do not save'), + inlineGroup: 'save_options', + min_version: 180000 + }, { + id: 'no_statistics', + label: gettext('Statistics'), + type: 'switch', + disabled: false, + group: gettext('Do not save'), + inlineGroup: 'save_options', + min_version: 180000 }]; } } 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..df4dcebc853 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,42 @@ 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, + only_statistics=True, + ), + url=RESTORE_JOB_URL, + expected_cmd='pg_restore', + expected_cmd_opts=['--no-policies', '--no-data', '--no-schema', + '--no-statistics', '--statistics-only'], + not_expected_cmd_opts=[], + expected_exit_code=[0, None], + server_min_version=180000, + message='Restore object with --no-policies, --no-data, ' + '--no-schema, --no-statistics, --statistics-only is not ' + 'supported by EPAS/PG server less than 18.0' + )), ] def setUp(self): From 8b90b670afe2015c88fb797610fd1c404b3b31a0 Mon Sep 17 00:00:00 2001 From: Dave Page Date: Mon, 17 Aug 2026 13:36:05 +0100 Subject: [PATCH 2/2] Complete the v18 option set and stop conflicting options being emitted pg_dump and pg_dumpall both accept --no-statistics, but only the Restore dialog offered it, so the Backup dialogs could dump statistics or nothing but statistics whilst having no way to say "everything except statistics". Backup now has the matching Statistics switch under Do not save, documented alongside the others. The larger problem is that several of these options cannot be combined, which I checked against the shipped utilities rather than inferring: pg_dump 18 reports "options -s/--schema-only and --statistics-only cannot be used together", and likewise for --data-only with --statistics-only, --statistics-only with --no-statistics, --schema-only with --no-schema and --data-only with --no-data. pg_restore and pg_dumpall reject the same pairs. The dialog disabled some of those combinations and not others, and nothing stopped a request arriving with both halves set, in which case the utility rejected the whole command. The backend now suppresses the narrower option in each pair, following the pattern --schema-only already used, and the switches under Do not save follow the state of their matching Only switch in both dialogs. The tests for the v18 options previously only ran where a v18 server was available, and silently skipped otherwise, so the new test_*_pg18_args_unit_test.py files call the argument builders directly with a mocked manager version. They cover the options being emitted on 18, suppressed on 17, and each conflicting pair resolving to the wider option. One existing scenario asserted a command pg_restore would have rejected, setting both --statistics-only and --no-statistics, so it is now split into two. --- docs/en_US/backup_dialog.rst | 4 + docs/en_US/backup_server_dialog.rst | 4 + web/pgadmin/tools/backup/__init__.py | 20 +++- .../tools/backup/static/js/backup.ui.js | 27 ++++- .../tests/test_backup_pg18_args_unit_test.py | 113 ++++++++++++++++++ web/pgadmin/tools/restore/__init__.py | 19 ++- .../tools/restore/static/js/restore.ui.js | 23 +++- .../test_restore_create_job_unit_test.py | 39 +++++- .../tests/test_restore_pg18_args_unit_test.py | 97 +++++++++++++++ 9 files changed, 325 insertions(+), 21 deletions(-) create mode 100644 web/pgadmin/tools/backup/tests/test_backup_pg18_args_unit_test.py create mode 100644 web/pgadmin/tools/restore/tests/test_restore_pg18_args_unit_test.py diff --git a/docs/en_US/backup_dialog.rst b/docs/en_US/backup_dialog.rst index dd7cf0ea0cb..ecbffc68cc8 100644 --- a/docs/en_US/backup_dialog.rst +++ b/docs/en_US/backup_dialog.rst @@ -159,6 +159,10 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg (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 diff --git a/docs/en_US/backup_server_dialog.rst b/docs/en_US/backup_server_dialog.rst index 0d088fcbdb2..08def536906 100644 --- a/docs/en_US/backup_server_dialog.rst +++ b/docs/en_US/backup_server_dialog.rst @@ -104,6 +104,10 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg (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 diff --git a/web/pgadmin/tools/backup/__init__.py b/web/pgadmin/tools/backup/__init__.py index 5e6291650bd..8d779273ef4 100644 --- a/web/pgadmin/tools/backup/__init__.py +++ b/web/pgadmin/tools/backup/__init__.py @@ -294,8 +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) + 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') @@ -320,8 +326,16 @@ def set_value(key, param, default_value=None, assertion=True): manager.version >= 150000) set_param('dns_no_role_passwords', '--no-role-passwords') set_param('no_policies', '--no-policies', manager.version >= 180000) - set_param('no_data', '--no-data', manager.version >= 180000) - set_param('no_schema', '--no-schema', 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') diff --git a/web/pgadmin/tools/backup/static/js/backup.ui.js b/web/pgadmin/tools/backup/static/js/backup.ui.js index 061611c306a..5b6bc9bc05c 100644 --- a/web/pgadmin/tools/backup/static/js/backup.ui.js +++ b/web/pgadmin/tools/backup/static/js/backup.ui.js @@ -313,21 +313,40 @@ export class SaveOptSchema extends BaseUISchema { 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', - disabled: false, group: gettext('Do not save'), inlineGroup: 'do_not_save', - min_version: 180000 + min_version: 180000, + deps: ['only_data'], + disabled: function(state) { + return state.only_data; + }, }, { id: 'no_schema', label: gettext('Schema'), type: 'switch', - disabled: false, group: gettext('Do not save'), inlineGroup: 'do_not_save', - min_version: 180000 + 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; + }, }]; } } 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 44e632958a1..7b80b676f1f 100644 --- a/web/pgadmin/tools/restore/__init__.py +++ b/web/pgadmin/tools/restore/__init__.py @@ -362,11 +362,22 @@ def get_restore_util_args(data, manager, server, driver, conn, filepath): # PostgreSQL 18 and above options if manager.version >= 180000: - set_param('only_statistics', '--statistics-only', data, args) + # 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) - set_param('no_data', '--no-data', data, args) - set_param('no_schema', '--no-schema', data, args) - set_param('no_statistics', '--no-statistics', 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) diff --git a/web/pgadmin/tools/restore/static/js/restore.ui.js b/web/pgadmin/tools/restore/static/js/restore.ui.js index 4c6cee3c0fa..5d0d2dd0ca4 100644 --- a/web/pgadmin/tools/restore/static/js/restore.ui.js +++ b/web/pgadmin/tools/restore/static/js/restore.ui.js @@ -245,29 +245,40 @@ export class RestoreSaveOptSchema extends BaseUISchema { 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', - disabled: false, group: gettext('Do not save'), inlineGroup: 'save_options', - min_version: 180000 + min_version: 180000, + deps: ['only_data'], + disabled: function(state) { + return state.only_data; + }, }, { id: 'no_schema', label: gettext('Schema'), type: 'switch', - disabled: false, group: gettext('Do not save'), inlineGroup: 'save_options', - min_version: 180000 + min_version: 180000, + deps: ['only_schema'], + disabled: function(state) { + return state.only_schema; + }, }, { id: 'no_statistics', label: gettext('Statistics'), type: 'switch', - disabled: false, group: gettext('Do not save'), inlineGroup: 'save_options', - min_version: 180000 + 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 df4dcebc853..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 @@ -431,17 +431,48 @@ class RestoreCreateJobTest(BaseTestGenerator): no_data=True, no_schema=True, no_statistics=True, - only_statistics=True, ), url=RESTORE_JOB_URL, expected_cmd='pg_restore', expected_cmd_opts=['--no-policies', '--no-data', '--no-schema', - '--no-statistics', '--statistics-only'], - not_expected_cmd_opts=[], + '--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, --statistics-only is not ' + '--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' )), ] 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)