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

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

No files matched your search

@@ -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',
@@ -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)
@@ -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
);
@@ -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 %}
@@ -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 %}
@@ -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)
@@ -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
};
+11 -8
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) ?