Added support to drop databases using the 'WITH (FORCE)' option. #6367

This commit is contained in:
Akshay Joshi
2023-06-19 15:04:40 +05:30
committed by GitHub
parent eef295f9d8
commit 557f33c4f9
15 changed files with 167 additions and 87 deletions

View File

@@ -26,16 +26,12 @@ When using main browser window, the following keyboard shortcuts are available:
+----------------------------+-------------------------------------------------------+
| Shift+Alt+d | Delete object |
+----------------------------+-------------------------------------------------------+
| Shift+Alt+m | Delete/Drop multiple objects |
+----------------------------+-------------------------------------------------------+
| Shift+Ctrl+[ | Dialog tab backward |
+----------------------------+-------------------------------------------------------+
| Shift+Ctrl+] | Dialog tab forward |
+----------------------------+-------------------------------------------------------+
| Shift+Alt+g | Direct debugging |
+----------------------------+-------------------------------------------------------+
| Shift+Alt+u | Drop Cascade multiple objects |
+----------------------------+-------------------------------------------------------+
| Shift+Alt+e | Edit object properties |
+----------------------------+-------------------------------------------------------+
| Shift+Alt+f | File main menu |

View File

@@ -58,12 +58,14 @@ following options (in alphabetical order):
| *Create* | Click *Create* to access a context menu that provides context-sensitive selections. |
| | Your selection opens a *Create* dialog for creating a new object. |
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
| *Delete/Drop* | Click to delete the currently selected object from the server. |
| *Delete* | Click to delete the currently selected object from the server. |
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
| *Delete (Cascade)* | Click to delete the currently selected object and all dependent objects from the server. |
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
| *Delete (Force)* | Click to delete the currently selected database with force option. |
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
| *Disconnect from server* | Click to disconnect from the currently selected server. |
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
| *Drop Cascade* | Click to delete the currently selected object and all dependent objects from the server. |
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
| *Properties...* | Click to review or modify the currently selected object's properties. |
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
| *Refresh* | Click to refresh the currently selected object. |

View File

@@ -20,6 +20,7 @@ Bundled PostgreSQL Utilities
New features
************
| `Issue #6367 <https://github.com/pgadmin-org/pgadmin4/issues/6367>`_ - Added support to drop databases using the 'WITH (FORCE)' option.
Housekeeping
************

View File

@@ -49,14 +49,16 @@ following selections (options appear in alphabetical order):
+---------------------------+---------------------------------------------------------------------------------------------------------------------------+
| *Debugging* | Click through to open the :ref:`Debug <debugger>` tool or to select *Set breakpoint* to stop or pause a script execution. |
+---------------------------+---------------------------------------------------------------------------------------------------------------------------+
| *Delete/Drop* | Click to delete the currently selected object from the server. |
| *Delete* | Click to delete the currently selected object from the server. |
+---------------------------+---------------------------------------------------------------------------------------------------------------------------+
| *Delete (Cascade)* | Click to delete the currently selected object and all dependent objects from the server. |
+---------------------------+---------------------------------------------------------------------------------------------------------------------------+
| *Delete (Force)* | Click to delete the currently selected database with force option. |
+---------------------------+---------------------------------------------------------------------------------------------------------------------------+
| *Disconnect Database...* | Click to terminate a database connection. |
+---------------------------+---------------------------------------------------------------------------------------------------------------------------+
| *Disconnect from server* | Click to disconnect from the currently selected server. |
+---------------------------+---------------------------------------------------------------------------------------------------------------------------+
| *Drop Cascade* | Click to delete the currently selected object and all dependent objects from the server. |
+---------------------------+---------------------------------------------------------------------------------------------------------------------------+
| *Debugging* | Click to access the :ref:`Debugger <debugger>` tool. |
+---------------------------+---------------------------------------------------------------------------------------------------------------------------+
| *Grant Wizard* | Click to access the :ref:`Grant Wizard <grant_wizard>` tool. |

View File

@@ -343,36 +343,6 @@ def register_browser_preferences(self):
fields=fields
)
self.preference.register(
'keyboard_shortcuts',
'grid_menu_drop_multiple',
gettext('Delete/Drop multiple objects'),
'keyboardshortcut',
{
'alt': True,
'shift': True,
'control': False,
'key': {'key_code': 77, 'char': 'm'}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=fields
)
self.preference.register(
'keyboard_shortcuts',
'grid_menu_drop_cascade_multiple',
gettext('Drop Cascade multiple objects'),
'keyboardshortcut',
{
'alt': True,
'shift': True,
'control': False,
'key': {'key_code': 85, 'char': 'u'}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=fields
)
self.preference.register(
'keyboard_shortcuts',
'context_menu',

View File

@@ -195,7 +195,9 @@ class DatabaseView(PGChildNodeView):
],
'vopts': [
{}, {'get': 'variable_options'}
]
],
'delete': [{'delete': 'delete'},
{'delete': 'delete'}]
})
def check_precondition(action=None):
@@ -1002,7 +1004,8 @@ class DatabaseView(PGChildNodeView):
sql = render_template(
"/".join([self.template_path, self._DELETE_SQL]),
datname=res, conn=self.conn
datname=res, conn=self.conn,
with_force=self.cmd == 'delete'
)
status, msg = default_conn.execute_scalar(sql)

View File

@@ -22,6 +22,14 @@ define('pgadmin.node.database', [
'pgadmin.authenticate.kerberos', 'pgadmin.browser.collection',
], function(gettext, url_for, $, pgAdmin, pgBrowser, Kerberos) {
function canDeleteWithForce(itemNodeData, item) {
let treeData = pgBrowser.tree.getTreeNodeHierarchy(item),
server = treeData['server'],
canDisconnect = !_.isUndefined(itemNodeData?.canDisconn) ? itemNodeData.canDisconn : true;
return (canDisconnect && server && server.version >= 130000);
}
if (!pgBrowser.Nodes['coll-database']) {
pgBrowser.Nodes['coll-database'] =
pgBrowser.Collection.extend({
@@ -33,6 +41,7 @@ define('pgadmin.node.database', [
canDrop: true,
selectParentNodeOnDelete: true,
canDropCascade: false,
canDropForce: canDeleteWithForce,
statsPrettifyFields: [gettext('Size'), gettext('Size of temporary files')],
});
}
@@ -90,9 +99,14 @@ define('pgadmin.node.database', [
data_disabled: gettext('Selected database is already connected.'),
},
},{
name: 'delete_database_force', node: 'database', module: this,
applies: ['object', 'context'], callback: 'delete_database_force',
category: 'delete', priority: 2, label: gettext('Delete (Force)'),
enable : canDeleteWithForce,
}, {
name: 'disconnect_database', node: 'database', module: this,
applies: ['object', 'context'], callback: 'disconnect_database',
category: 'drop', priority: 5, label: gettext('Disconnect from database'),
category: 'disconnect', priority: 5, label: gettext('Disconnect from database'),
enable : 'is_connected',data: {
data_disabled: gettext('Selected database is already disconnected.'),
},
@@ -123,7 +137,6 @@ define('pgadmin.node.database', [
// If server is less than 10 then do not allow 'create' menu
return server && server.version >= 100000;
},
is_not_connected: function(node) {
return (node && !node.connected && node.allowConn);
},
@@ -310,6 +323,10 @@ define('pgadmin.node.database', [
if (!d.allowConn) return;
pgBrowser.Node.callbacks.refresh.apply(this, arguments);
},
delete_database_force: function(args, item) {
pgBrowser.Node.callbacks.delete_obj.apply(this, [{'url': 'delete'}, item]);
}
},
getSchema: function(treeNodeInfo, itemNodeData) {
let c_types = ()=>getNodeAjaxOptions('get_ctypes', this, treeNodeInfo, itemNodeData, {
@@ -437,7 +454,6 @@ define('pgadmin.node.database', [
} else {
Notify.success(res.info);
}
// obj.trigger('connected', obj, _item, _data);
pgBrowser.Events.trigger(
'pgadmin:database:connected', _item, _data
);

View File

@@ -4,5 +4,5 @@ SELECT db.datname as name FROM pg_catalog.pg_database as db WHERE db.oid = {{did
{% endif %}
{# Using name from above query we will drop the database #}
{% if datname %}
DROP DATABASE IF EXISTS {{ conn|qtIdent(datname) }};
DROP DATABASE IF EXISTS {{ conn|qtIdent(datname) }}{% if with_force %} WITH (FORCE){%endif%};
{% endif %}

View File

@@ -1,10 +0,0 @@
{# We need database name before we execute drop #}
{% if db_ids %}
SELECT db.datname as name FROM pg_catalog.pg_database as db WHERE db.oid in {{db_ids}};
{% endif %}
{# Using name from above query we will drop the database #}
{% if dbname_array %}
{% for db in dbname_array %}
DROP DATABASE {{ conn|qtIdent(db.name) }};
{% endfor %}
{% endif %}

View File

@@ -0,0 +1,64 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2023, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import uuid
import json
from pgadmin.utils import server_utils
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
class DatabaseMultipleDeleteForceTestCase(BaseTestGenerator):
""" This class will delete the multiple database with force option under
last added server. """
scenarios = [
# Fetching default URL for database node.
('Delete with Force URL', dict(url='/browser/database/delete/'))
]
def setUp(self):
if self.server_information['server_version'] < 130000:
message = "Delete with Force are not supported by PG < 130000."
self.skipTest(message)
self.db_names = ["db_delete_%s" % str(uuid.uuid4())[1:8],
"db_delete_%s" % str(uuid.uuid4())[1:8]]
self.db_ids = [utils.create_database(self.server, self.db_names[0]),
utils.create_database(self.server, self.db_names[1])]
self.server_id = parent_node_dict["server"][-1]["server_id"]
def runTest(self):
""" This function will delete the databases."""
server_response = server_utils.connect_server(self, self.server_id)
if server_response["data"]["connected"]:
data = {'ids': self.db_ids}
response = self.tester.delete(
self.url + str(utils.SERVER_GROUP) + '/' +
str(self.server_id) + '/',
follow_redirects=True,
data=json.dumps(data),
content_type='html/json')
self.assertEqual(response.status_code, 200)
else:
raise Exception("Could not connect to server to delete the "
"database.")
def tearDown(self):
"""This function drop the added databases"""
connection = utils.get_db_connection(self.server['db'],
self.server['username'],
self.server['db_password'],
self.server['host'],
self.server['port'],
self.server['sslmode'])
utils.drop_database_multiple(connection, self.db_names)

View File

@@ -39,8 +39,6 @@ _.extend(pgBrowser.keyboardNavigation, {
'sub_menu_refresh': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'sub_menu_refresh').value),
'context_menu': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'context_menu').value),
'direct_debugging': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'direct_debugging').value),
'drop_multiple_objects': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'grid_menu_drop_multiple').value),
'drop_cascade_multiple_objects': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'grid_menu_drop_cascade_multiple').value),
'add_grid_row': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'add_grid_row').value),
'open_quick_search': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'open_quick_search').value),
@@ -62,8 +60,6 @@ _.extend(pgBrowser.keyboardNavigation, {
'bindSubMenuRefresh': {'shortcuts': this.keyboardShortcut.sub_menu_refresh, 'bindElem': '#tree'}, // Sub menu - Refresh object,
'bindContextMenu': {'shortcuts': this.keyboardShortcut.context_menu}, // Sub menu - Open context menu,
'bindDirectDebugging': {'shortcuts': this.keyboardShortcut.direct_debugging}, // Sub menu - Direct Debugging
'bindDropMultipleObjects': {'shortcuts': this.keyboardShortcut.drop_multiple_objects}, // Grid Menu Drop Multiple
'bindDropCascadeMultipleObjects': {'shortcuts': this.keyboardShortcut.drop_cascade_multiple_objects}, // Grid Menu Drop Cascade Multiple
'bindAddGridRow': {'shortcuts': this.keyboardShortcut.add_grid_row}, // Subnode Grid Add Row
'bindOpenQuickSearch': {'shortcuts': this.keyboardShortcut.open_quick_search}, // Subnode Grid Refresh Row
};

View File

@@ -144,7 +144,7 @@ define('pgadmin.browser.node', [
applies: ['object', 'context'],
callback: 'delete_obj',
priority: self.dropPriority,
label: (self.dropAsRemove) ? gettext('Remove %s', self.label) : gettext('Delete/Drop'),
label: (self.dropAsRemove) ? gettext('Remove %s', self.label) : gettext('Delete'),
data: {
'url': 'drop',
data_disabled: gettext('The selected tree node does not support this option.'),
@@ -162,8 +162,8 @@ define('pgadmin.browser.node', [
module: self,
applies: ['object', 'context'],
callback: 'delete_obj',
priority: 3,
label: gettext('Drop Cascade'),
priority: 2,
label: gettext('Delete (Cascade)'),
data: {
'url': 'delete',
},
@@ -657,11 +657,14 @@ define('pgadmin.browser.node', [
let msg, title;
if (input.url == 'delete') {
if (input.url == 'delete' && d._type === 'database') {
msg = gettext('Delete database with the force option will attempt to terminate all existing connections to the "%s" database. Are you sure you want to proceed?', d.label);
title = gettext('Delete FORCE %s?', obj.label);
msg = gettext('Are you sure you want to drop %s "%s" and all the objects that depend on it?',
} else if (input.url == 'delete') {
msg = gettext('Are you sure you want to delete %s "%s" and all the objects that depend on it?',
obj.label.toLowerCase(), d.label);
title = gettext('DROP CASCADE %s?', obj.label);
title = gettext('Delete CASCADE %s?', obj.label);
if (!(_.isFunction(obj.canDropCascade) ?
obj.canDropCascade.apply(obj, [d, i]) : obj.canDropCascade)) {
@@ -676,8 +679,8 @@ define('pgadmin.browser.node', [
msg = gettext('Are you sure you want to remove %s "%s"?', obj.label.toLowerCase(), d.label);
title = gettext('Remove %s?', obj.label);
} else {
msg = gettext('Are you sure you want to drop %s "%s"?', obj.label.toLowerCase(), d.label);
title = gettext('Drop %s?', obj.label);
msg = gettext('Are you sure you want to delete %s "%s"?', obj.label.toLowerCase(), d.label);
title = gettext('Delete %s?', obj.label);
}
if (!(_.isFunction(obj.canDrop) ?

View File

@@ -21,8 +21,10 @@ import PropTypes from 'prop-types';
import { PgIconButton } from '../../static/js/components/Buttons';
import DeleteIcon from '@material-ui/icons/Delete';
import DeleteSweepIcon from '@material-ui/icons/DeleteSweep';
import DeleteForeverIcon from '@material-ui/icons/DeleteForever';
import EmptyPanelMessage from '../../static/js/components/EmptyPanelMessage';
import Loader from 'sources/components/Loader';
import { evalFunc } from '../../static/js/utils';
const useStyles = makeStyles((theme) => ({
emptyPanel: {
@@ -139,7 +141,7 @@ export function CollectionNodeView({
if (selRows.length === 0) {
Notify.alert(
gettext('Drop Multiple'),
gettext('Delete Multiple'),
gettext('Please select at least one object to delete.')
);
return;
@@ -150,23 +152,31 @@ export function CollectionNodeView({
if (type === 'dropCascade') {
url = selNode.generate_url(selItem, 'delete');
msg = gettext(
'Are you sure you want to drop all the selected objects and all the objects that depend on them?'
'Are you sure you want to delete all the selected objects and all the objects that depend on them?'
);
title = gettext('DROP CASCADE multiple objects?');
title = gettext('Delete CASCADE multiple objects?');
} else if (type === 'dropForce') {
url = selNode.generate_url(selItem, 'delete');
msg = gettext(
'Delete databases with the force option will attempt to terminate all the existing connections to the selected databases. Are you sure you want to proceed?'
);
title = gettext('Delete FORCE multiple objects?');
} else {
url = selNode.generate_url(selItem, 'drop');
msg = gettext('Are you sure you want to drop all the selected objects?');
title = gettext('DROP multiple objects?');
msg = gettext('Are you sure you want to delete all the selected objects?');
title = gettext('Delete multiple objects?');
}
const api = getApiInstance();
let dropNodeProperties = function () {
setLoaderText(gettext('Deleting Objects...'));
api
.delete(url, {
data: JSON.stringify({ ids: selRows }),
contentType: 'application/json; charset=utf-8',
})
.then(function (res) {
setLoaderText('');
if (res.success == 0) {
Notify.alert(res.errormsg, res.info);
}
@@ -174,8 +184,9 @@ export function CollectionNodeView({
setReload(!reload);
})
.catch(function (error) {
setLoaderText('');
Notify.alert(
gettext('Error dropping %s', selectedItemData._label.toLowerCase()),
gettext('Error deleting %s', selectedItemData._label.toLowerCase()),
_.isUndefined(error.response) ? error.message : error.response.data.errormsg
);
});
@@ -200,7 +211,7 @@ export function CollectionNodeView({
let tableColumns = [];
let column = {};
setLoaderText('Loading...');
setLoaderText(gettext('Loading...'));
if (itemNodeData._type.indexOf('coll-') > -1 && !_.isUndefined(nodeObj.getSchema)) {
schemaRef.current = nodeObj.getSchema?.call(nodeObj, treeNodeInfo, itemNodeData);
@@ -269,41 +280,59 @@ export function CollectionNodeView({
}, [itemNodeData, node, item, reload]);
const CustomHeader = () => {
const canDrop = evalFunc(node, node.canDrop, itemNodeData, item, treeNodeInfo);
const canDropCascade = evalFunc(node, node.canDropCascade, itemNodeData, item, treeNodeInfo);
const canDropForce = evalFunc(node, node.canDropForce, itemNodeData, item, treeNodeInfo);
return (
<Box >
<PgIconButton
className={classes.dropButton}
icon={<DeleteIcon/>}
aria-label="Delete/Drop"
title={gettext('Delete/Drop')}
aria-label="Delete"
title={gettext('Delete')}
onClick={() => {
onDrop('drop');
}}
disabled={
(selectedObject.length > 0)
? !node.canDrop
? !canDrop
: true
}
></PgIconButton>
<PgIconButton
{node.type !== 'coll-database' ? <PgIconButton
className={classes.dropButton}
icon={<DeleteSweepIcon />}
aria-label="Drop Cascade"
title={gettext('Drop Cascade')}
aria-label="Delete Cascade"
title={gettext('Delete (Cascade)')}
onClick={() => {
onDrop('dropCascade');
}}
disabled={
(selectedObject.length > 0)
? !node.canDropCascade
? !canDropCascade
: true
}
></PgIconButton>
></PgIconButton> :
<PgIconButton
className={classes.dropButton}
icon={<DeleteForeverIcon />}
aria-label="Delete Force"
title={gettext('Delete (Force)')}
onClick={() => {
onDrop('dropForce');
}}
disabled={
(selectedObject.length > 0)
? !canDropForce
: true
}
></PgIconButton>}
</Box>);
};
return (
<Theme className='obj_properties'>
<Loader message={loaderText}/>
<Box className={classes.propertiesPanel}>
{data.length > 0 ?
(

View File

@@ -358,9 +358,9 @@ function checkBinaryPathExists(binaryPathArray, selectedServerVersion) {
}
/* If a function, then evaluate */
export function evalFunc(obj, func, param) {
export function evalFunc(obj, func, ...param) {
if(_.isFunction(func)) {
return func.apply(obj, [param]);
return func.apply(obj, [...param]);
}
return func;
}

View File

@@ -564,7 +564,11 @@ def drop_database(connection, database_name):
if pg_cursor.fetchall():
old_isolation_level = connection.isolation_level
set_isolation_level(connection, 0)
pg_cursor.execute('''DROP DATABASE "%s"''' % database_name)
if connection.info.server_version >= 130000:
pg_cursor.execute(
'''DROP DATABASE "%s" WITH (FORCE)''' % database_name)
else:
pg_cursor.execute('''DROP DATABASE "%s"''' % database_name)
set_isolation_level(connection, old_isolation_level)
connection.commit()
connection.close()
@@ -594,7 +598,11 @@ def drop_database_multiple(connection, database_names):
if pg_cursor.fetchall():
old_isolation_level = connection.isolation_level
set_isolation_level(connection, 0)
pg_cursor.execute('''DROP DATABASE "%s"''' % database_name)
if connection.info.server_version >= 130000:
pg_cursor.execute(
'''DROP DATABASE "%s" WITH (FORCE)''' % database_name)
else:
pg_cursor.execute('''DROP DATABASE "%s"''' % database_name)
set_isolation_level(connection, old_isolation_level)
connection.commit()
connection.close()