Add the PostgreSQL 18 options to the Backup, Backup Server and Restore dialogs. #9064, #9066 (#10060)
Add --statistics-only, --no-policies, --no-data, --no-schema, --no-statistics, --statistics and --sequence-data where the utilities support them, gated on server version 18, and suppress combinations that pg_dump, pg_dumpall and pg_restore would reject. The pg_dumpall --format and pg_restore --exclude-database/--globals-only options were reverted before PostgreSQL 18.0 and so are not included.
This commit is contained in:
@@ -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,15 @@ 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. It cannot be combined with *Only
|
||||
data*, *Only schemas* or *Do not save* *Statistics*. **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.
|
||||
|
||||
|
||||
@@ -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,15 @@ 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. It cannot be combined with *Only
|
||||
data*, *Only schemas* or *Do not save* *Statistics*. **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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -294,6 +294,14 @@ def _get_args_params_values(data, conn, backup_obj_type, backup_file, server,
|
||||
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 _get_args_params_values(data, conn, backup_obj_type, backup_file, server,
|
||||
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,15 @@ def _get_args_params_values(data, conn, backup_obj_type, backup_file, server,
|
||||
set_param('verbose', '--verbose')
|
||||
set_param('dqoute', '--quote-all-identifiers')
|
||||
set_param('use_set_session_auth', '--use-set-session-authorization')
|
||||
# pg_dump rejects --statistics alongside --data-only, --schema-only or
|
||||
# --no-statistics. Statistics are not dumped by default, so dropping
|
||||
# --statistics gives the result the other option asked for.
|
||||
set_param('statistics', '--statistics',
|
||||
manager.version >= 180000 and
|
||||
not data.get('only_data', None) and
|
||||
not data.get('only_schema', None) and
|
||||
not data.get('no_statistics', None))
|
||||
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)
|
||||
|
||||
@@ -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', 'statistics'],
|
||||
disabled: function(state) {
|
||||
return state.only_statistics || state.statistics;
|
||||
},
|
||||
}];
|
||||
}
|
||||
}
|
||||
@@ -384,6 +449,25 @@ export class MiscellaneousSchema extends BaseUISchema {
|
||||
disabled: false,
|
||||
group: gettext('Miscellaneous'),
|
||||
inlineGroup: 'miscellaneous',
|
||||
}, {
|
||||
id: 'statistics',
|
||||
label: gettext('Dump statistics'),
|
||||
type: 'switch',
|
||||
deps: ['only_data', 'only_schema', 'no_statistics'],
|
||||
disabled: function(state) {
|
||||
return state.only_data || state.only_schema || state.no_statistics;
|
||||
},
|
||||
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'),
|
||||
|
||||
@@ -1362,6 +1362,134 @@ class BackupCreateJobTest(BaseTestGenerator):
|
||||
expected_env={
|
||||
'PGDATABASE': 'host=127.0.0.1 port=5433 dbname=postgres'},
|
||||
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'
|
||||
))
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
##########################################################################
|
||||
#
|
||||
# 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, and --statistics conflicts with
|
||||
--data-only, --schema-only and --no-statistics, 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'],
|
||||
)),
|
||||
('Only data wins over statistics', dict(
|
||||
version=V18,
|
||||
data=dict(only_data=True, statistics=True),
|
||||
expected=['--data-only'],
|
||||
not_expected=['--statistics'],
|
||||
)),
|
||||
('Only schema wins over statistics', dict(
|
||||
version=V18,
|
||||
data=dict(only_schema=True, statistics=True),
|
||||
expected=['--schema-only'],
|
||||
not_expected=['--statistics'],
|
||||
)),
|
||||
('No statistics wins over statistics', dict(
|
||||
version=V18,
|
||||
data=dict(no_statistics=True, statistics=True),
|
||||
expected=['--no-statistics'],
|
||||
not_expected=['--statistics'],
|
||||
)),
|
||||
('Statistics may be combined with only statistics', dict(
|
||||
version=V18,
|
||||
data=dict(only_statistics=True, statistics=True),
|
||||
expected=['--statistics-only', '--statistics'],
|
||||
not_expected=[],
|
||||
)),
|
||||
]
|
||||
|
||||
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)
|
||||
@@ -371,6 +371,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,
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,6 +490,73 @@ class RestoreCreateJobTest(BaseTestGenerator):
|
||||
# connect to a database named after the login role.
|
||||
expected_status_code=400,
|
||||
)),
|
||||
('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):
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user