Add support for restoring plain SQL database dumps. #5871

pull/8530/head
Aditya Toshniwal 2025-03-05 18:22:44 +05:30 committed by GitHub
parent d7faa85338
commit 7a25da9b06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 232 additions and 50 deletions

View File

@ -27,6 +27,8 @@ restore process:
* Select *Custom or tar* to restore from a custom archive file to create a
copy of the backed-up object.
* Select *Plain* to restore a plain SQL backup. When selecting this option
all the other options will not be applicable.
* Select *Directory* to restore from a compressed directory-format archive.
* Enter the complete path to the backup file in the *Filename* field.

View File

@ -32,6 +32,7 @@ import {
} from './hooks';
import { registerView, View } from './registry';
import { createFieldControls, listenDepChanges } from './utils';
import FormViewTab from './FormViewTab';
const ErrorMessageBox = () => {
const [key, setKey] = useState(0);
@ -181,14 +182,8 @@ export default function FormView({
action={(ref) => ref?.updateIndicator()}
>{
finalGroups.map((tabGroup, idx) =>
<Tab
key={tabGroup.id}
label={tabGroup.label}
data-test={tabGroup.id}
className={
tabGroup.hasError &&
tabValue != idx ? 'tab-with-error' : ''
}
<FormViewTab
key={tabGroup.id} tabGroup={tabGroup} idx={idx} tabValue={tabValue}
/>
)
}{hasSQLTab &&

View File

@ -0,0 +1,45 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { Tab } from '@mui/material';
import React, { useContext, useState } from 'react';
import { useFieldOptions, useSchemaStateSubscriber } from './hooks';
import { SchemaStateContext } from './SchemaState';
import PropTypes from 'prop-types';
export default function FormViewTab({tabGroup, idx, tabValue, ...props}) {
const accessPath = [tabGroup.id];
const [refreshKey, setRefreshKey] = useState(0);
const subscriberManager = useSchemaStateSubscriber(setRefreshKey);
const schemaState = useContext(SchemaStateContext);
const options = useFieldOptions(accessPath, schemaState, subscriberManager);
return (
<Tab
key={refreshKey}
label={tabGroup.label}
data-test={tabGroup.id}
iconPosition='start'
disabled={options.disabled}
className={
tabGroup.hasError &&
tabValue != idx ? 'tab-with-error' : ''
}
{...props}
/>
);
};
FormViewTab.muiName = Tab.muiName;
FormViewTab.propTypes = {
tabGroup: PropTypes.object,
idx: PropTypes.number,
tabValue: PropTypes.number,
};

View File

@ -788,13 +788,17 @@ function getFinalTheme(baseTheme) {
MuiTab: {
styleOverrides: {
root: {
'&.MuiTab-textColorPrimary':{
'&:not(.Mui-disabled).MuiTab-textColorPrimary':{
color: baseTheme.palette.text.primary,
},
'&.Mui-selected': {
color: baseTheme.otherVars.activeColor,
},
}
},
icon: {
fontSize: '1rem',
marginRight: '2px',
},
}
},
MuiBackdrop: {

View File

@ -141,18 +141,18 @@ def _get_create_req_data():
data = json.loads(request.data)
try:
_file = filename_with_file_manager_path(data['file'])
filepath = filename_with_file_manager_path(data['file'])
except Exception as e:
return True, internal_server_error(errormsg=str(e)), data, None
if _file is None:
if filepath is None:
return True, make_json_response(
status=410,
success=0,
errormsg=_("File could not be found.")
), data, _file
), data, filepath
return False, '', data, _file
return False, '', data, filepath
def _connect_server(sid):
@ -263,15 +263,15 @@ def set_multiple(key, param, data, args, driver, conn, with_schema=True):
return False
def _set_args_param_values(data, manager, server, driver, conn, _file):
def get_restore_util_args(data, manager, server, driver, conn, filepath):
"""
add args to the list.
return the args for the command
:param data: Data.
:param manager: Manager.
:param server: Server.
:param driver: Driver.
:param conn: Connection.
:param _file: File.
:param filepath: File.
:return: args list.
"""
args = []
@ -347,11 +347,56 @@ def _set_args_param_values(data, manager, server, driver, conn, _file):
False)
set_multiple('indexes', '--index', data, args, driver, conn, False)
args.append(fs_short_path(_file))
args.append(fs_short_path(filepath))
return args
def get_sql_util_args(data, manager, server, filepath):
"""
return the args for the command
:param data: Data.
:param manager: Manager.
:param server: Server.
:param filepath: File.
:return: args list.
"""
args = [
'--host',
manager.local_bind_host if manager.use_ssh_tunnel else server.host,
'--port',
str(manager.local_bind_port) if manager.use_ssh_tunnel
else str(server.port),
'--username', server.username, '--dbname',
data['database'],
'--file', fs_short_path(filepath)
]
return args
def use_restore_utility(data, manager, server, driver, conn, filepath):
utility = manager.utility('restore')
ret_val = does_utility_exist(utility)
if ret_val:
return ret_val, None, None
args = get_restore_util_args(data, manager, server, driver, conn, filepath)
return None, utility, args
def use_sql_utility(data, manager, server, filepath):
utility = manager.utility('sql')
ret_val = does_utility_exist(utility)
if ret_val:
return ret_val, None, None
args = get_sql_util_args(data, manager, server, filepath)
return None, utility, args
@blueprint.route('/job/<int:sid>', methods=['POST'], endpoint='create_job')
@pga_login_required
def create_restore_job(sid):
@ -364,7 +409,7 @@ def create_restore_job(sid):
Returns:
None
"""
is_error, errmsg, data, _file = _get_create_req_data()
is_error, errmsg, data, filepath = _get_create_req_data()
if is_error:
return errmsg
@ -372,16 +417,19 @@ def create_restore_job(sid):
if is_error:
return errmsg
utility = manager.utility('restore')
ret_val = does_utility_exist(utility)
if ret_val:
if data['format'] == 'plain':
error_msg, utility, args = use_sql_utility(
data, manager, server, filepath)
else:
error_msg, utility, args = use_restore_utility(
data, manager, server, driver, conn, filepath)
if error_msg is not None:
return make_json_response(
success=0,
errormsg=ret_val
errormsg=error_msg
)
args = _set_args_param_values(data, manager, server, driver, conn, _file)
try:
p = BatchProcess(
desc=RestoreMessage(

View File

@ -81,7 +81,8 @@ define('tools.restore', [
()=>getRestoreDisableOptionSchema({nodeInfo: treeNodeInfo}),
()=>getRestoreMiscellaneousSchema({nodeInfo: treeNodeInfo}),
{
role: ()=>getNodeListByName('role', treeNodeInfo, itemNodeData)
role: ()=>getNodeListByName('role', treeNodeInfo, itemNodeData),
nodeType: itemNodeData._type,
},
treeNodeInfo,
pgBrowser

View File

@ -343,12 +343,37 @@ export default class RestoreSchema extends BaseUISchema {
this.getRestoreMiscellaneousSchema = restoreMiscellaneousSchema;
this.treeNodeInfo = treeNodeInfo;
this.pgBrowser = pgBrowser;
this.formatOptions = this.getFormatOptions();
}
get idAttribute() {
return 'id';
}
isPlainFormat(state) {
return state.format == 'plain';
}
getFormatOptions() {
const options = [{
label: gettext('Custom or tar'),
value: 'custom',
}, {
label: gettext('Directory'),
value: 'directory',
}];
if(this.fieldOptions.nodeType == 'database') {
options.splice(1, 0, {
label: gettext('Plain'),
value: 'plain',
});
}
return options;
}
get baseFields() {
let obj = this;
return [{
@ -356,14 +381,15 @@ export default class RestoreSchema extends BaseUISchema {
label: gettext('Format'),
disabled: false,type: 'select',
controlProps: { allowClear: false, width: '100%' },
options: [{
label: gettext('Custom or tar'),
value: 'custom',
},
{
label: gettext('Directory'),
value: 'directory',
}],
options: this.formatOptions,
depChange: (state) => {
if(state.format == 'plain') {
return {
no_of_jobs: undefined,
role: undefined,
};
}
}
}, {
id: 'file',
label: gettext('Filename'),
@ -378,11 +404,12 @@ export default class RestoreSchema extends BaseUISchema {
};
},
disabled: false,
deps: ['format']
deps: ['format'],
}, {
id: 'no_of_jobs',
label: gettext('Number of jobs'),
type: 'int',
disabled: obj.isPlainFormat,
}, {
id: 'role',
label: gettext('Role name'),
@ -392,35 +419,45 @@ export default class RestoreSchema extends BaseUISchema {
controlProps: {
allowClear: false,
},
disabled: obj.isPlainFormat,
}, {
id: 'data-options', type: 'group', label: gettext('Data Options'),
disabled: function(state) {
return obj.isPlainFormat(state);
}, deps: ['format']
}, {
type: 'nested-fieldset',
label: gettext('Sections'),
group: gettext('Data Options'),
group: 'data-options',
schema:obj.getSectionSchema(),
visible: true
visible: true,
}, {
type: 'nested-fieldset',
label: gettext('Type of objects'),
group: gettext('Data Options'),
group: 'data-options',
schema:obj.getRestoreTypeObjSchema(),
visible: true
visible: true,
}, {
type: 'nested-fieldset',
label: gettext('Do not save'),
group: gettext('Data Options'),
group: 'data-options',
schema:obj.getRestoreSaveOptSchema(),
visible: true
visible: true,
}, {
id: 'query-options', type: 'group', label: gettext('Query Options'),
disabled: function(state) {
return obj.isPlainFormat(state);
}, deps: ['format']
}, {
id: 'include_create_database',
label: gettext('Include CREATE DATABASE statement'),
type: 'switch',
disabled: false,
group: gettext('Query Options')
group: 'query-options'
}, {
id: 'clean',
label: gettext('Clean before restore'),
type: 'switch',
group: gettext('Query Options'),
group: 'query-options',
inlineGroup: 'clean',
disabled: function(state) {
if(obj.selectedNodeType === 'function' || obj.selectedNodeType === 'trigger_function') {
@ -432,7 +469,7 @@ export default class RestoreSchema extends BaseUISchema {
id: 'if_exists',
label: gettext('Include IF EXISTS clause'),
type: 'switch',
group: gettext('Query Options'),
group: 'query-options',
inlineGroup: 'clean',
deps: ['clean'],
disabled: function(state) {
@ -447,29 +484,39 @@ export default class RestoreSchema extends BaseUISchema {
label: gettext('Single transaction'),
type: 'switch',
disabled: false,
group: gettext('Query Options'),
group: 'query-options',
}, {
id: 'table-options', type: 'group', label: gettext('Table Options'),
disabled: function(state) {
return obj.isPlainFormat(state);
}, deps: ['format']
}, {
id: 'enable_row_security',
label: gettext('Enable row security'),
type: 'switch',
disabled: false,
group: gettext('Table Options'),
group: 'table-options',
}, {
id: 'no_data_fail_table',
label: gettext('No data for failed tables'),
type: 'switch',
disabled: false,
group: gettext('Table Options'),
group: 'table-options',
}, {
id: 'options', type: 'group', label: gettext('Options'),
disabled: function(state) {
return obj.isPlainFormat(state);
}, deps: ['format']
}, {
type: 'nested-fieldset',
label: gettext('Disable'),
group: gettext('Options'),
group: 'options',
schema:obj.getRestoreDisableOptionSchema(),
visible: true
}, {
type: 'nested-fieldset',
label: gettext('Miscellaneous / Behavior'),
group: gettext('Options'),
group: 'options',
schema:obj.getRestoreMiscellaneousSchema(),
visible: true
}];

View File

@ -24,6 +24,28 @@ RESTORE_JOB_URL = '/restore/job/{0}'
class RestoreCreateJobTest(BaseTestGenerator):
"""Test the RestoreCreateJob class"""
scenarios = [
('When restore object with plain format',
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='plain',
database='postgres'
),
url=RESTORE_JOB_URL,
expected_cmd='psql',
expected_cmd_opts=['--file'],
not_expected_cmd_opts=[],
expected_exit_code=[0, None]
)),
('When restore object with default options',
dict(
class_params=dict(
@ -46,6 +68,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
database='postgres'
),
url=RESTORE_JOB_URL,
expected_cmd='pg_restore',
expected_cmd_opts=['--verbose'],
not_expected_cmd_opts=[],
expected_exit_code=[0, None]
@ -72,6 +95,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
database='postgres'
),
url=RESTORE_JOB_URL,
expected_cmd='pg_restore',
expected_cmd_opts=['--verbose', '--format=d'],
not_expected_cmd_opts=[],
expected_exit_code=[0, None]
@ -103,6 +127,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
only_schema=True
),
url=RESTORE_JOB_URL,
expected_cmd='pg_restore',
expected_cmd_opts=['--verbose', '--jobs', '2',
'--section=pre-data', '--section=data',
'--section=post-data'],
@ -136,6 +161,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
dns_owner=True
),
url=RESTORE_JOB_URL,
expected_cmd='pg_restore',
expected_cmd_opts=['--verbose', '--data-only'],
not_expected_cmd_opts=[],
# Below options should be enabled once we fix the issue #3368
@ -167,6 +193,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
only_data=False
),
url=RESTORE_JOB_URL,
expected_cmd='pg_restore',
expected_cmd_opts=['--no-owner',
'--no-tablespaces',
'--no-privileges'],
@ -200,6 +227,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
only_data=False
),
url=RESTORE_JOB_URL,
expected_cmd='pg_restore',
expected_cmd_opts=['--no-comments', '--no-publications',
'--no-subscriptions', '--no-security-labels'],
not_expected_cmd_opts=[],
@ -231,6 +259,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
dns_table_access_method=True
),
url=RESTORE_JOB_URL,
expected_cmd='pg_restore',
expected_cmd_opts=['--no-table-access-method'],
not_expected_cmd_opts=[],
expected_exit_code=[0, None],
@ -262,6 +291,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
if_exists=True,
),
url=RESTORE_JOB_URL,
expected_cmd='pg_restore',
expected_cmd_opts=['--create', '--clean',
'--single-transaction', '--if-exists'],
not_expected_cmd_opts=[],
@ -291,6 +321,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
only_schema=False
),
url=RESTORE_JOB_URL,
expected_cmd='pg_restore',
expected_cmd_opts=['--enable-row-security',
'--no-data-for-failed-tables'],
not_expected_cmd_opts=[],
@ -318,6 +349,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
only_schema=False
),
url=RESTORE_JOB_URL,
expected_cmd='pg_restore',
expected_cmd_opts=['--disable-triggers'],
not_expected_cmd_opts=[],
expected_exit_code=[0, None]
@ -344,6 +376,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
exit_on_error=True,
),
url=RESTORE_JOB_URL,
expected_cmd='pg_restore',
expected_cmd_opts=['--exit-on-error',
'--use-set-session-authorization'],
not_expected_cmd_opts=[],
@ -370,6 +403,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
exclude_schema="sch*"
),
url=RESTORE_JOB_URL,
expected_cmd='pg_restore',
expected_cmd_opts=['--exclude-schema', 'sch*'],
not_expected_cmd_opts=[],
expected_exit_code=[0, None]
@ -459,6 +493,11 @@ class RestoreCreateJobTest(BaseTestGenerator):
self.assertTrue(restore_message_mock.called)
self.assertTrue(batch_process_mock.called)
if self.expected_cmd:
self.assertIn(
self.expected_cmd,
batch_process_mock.call_args_list[0][1]['cmd']
)
if self.expected_cmd_opts:
for opt in self.expected_cmd_opts:
self.assertIn(

View File

@ -23,6 +23,7 @@ describe('RestoreSchema', ()=>{
{
role: ()=>[],
encoding: ()=>[],
nodeType: '',
},
{server: {version: 11000}},
pgAdmin.pgBrowser