diff --git a/docs/en_US/keyboard_shortcuts.rst b/docs/en_US/keyboard_shortcuts.rst index 63216151e..836ccec53 100644 --- a/docs/en_US/keyboard_shortcuts.rst +++ b/docs/en_US/keyboard_shortcuts.rst @@ -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 | diff --git a/docs/en_US/menu_bar.rst b/docs/en_US/menu_bar.rst index 5a9e5ffdb..576d2fbc8 100644 --- a/docs/en_US/menu_bar.rst +++ b/docs/en_US/menu_bar.rst @@ -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. | diff --git a/docs/en_US/release_notes_7_4.rst b/docs/en_US/release_notes_7_4.rst index 64a5ce5e3..6d2f55470 100644 --- a/docs/en_US/release_notes_7_4.rst +++ b/docs/en_US/release_notes_7_4.rst @@ -20,6 +20,7 @@ Bundled PostgreSQL Utilities New features ************ + | `Issue #6367 `_ - Added support to drop databases using the 'WITH (FORCE)' option. Housekeeping ************ diff --git a/docs/en_US/tree_control.rst b/docs/en_US/tree_control.rst index 0a15d5ba9..c53c1bd92 100644 --- a/docs/en_US/tree_control.rst +++ b/docs/en_US/tree_control.rst @@ -49,14 +49,16 @@ following selections (options appear in alphabetical order): +---------------------------+---------------------------------------------------------------------------------------------------------------------------+ | *Debugging* | Click through to open the :ref:`Debug ` 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 ` tool. | +---------------------------+---------------------------------------------------------------------------------------------------------------------------+ | *Grant Wizard* | Click to access the :ref:`Grant Wizard ` tool. | diff --git a/web/pgadmin/browser/register_browser_preferences.py b/web/pgadmin/browser/register_browser_preferences.py index 59c33bbf8..331c6e5af 100644 --- a/web/pgadmin/browser/register_browser_preferences.py +++ b/web/pgadmin/browser/register_browser_preferences.py @@ -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', diff --git a/web/pgadmin/browser/server_groups/servers/databases/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/__init__.py index 677def01e..0f223b861 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/__init__.py @@ -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) diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js index 909dd4ebf..9d042ecea 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js +++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js @@ -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 ); diff --git a/web/pgadmin/browser/server_groups/servers/databases/templates/databases/sql/default/delete.sql b/web/pgadmin/browser/server_groups/servers/databases/templates/databases/sql/default/delete.sql index 2b52461d5..71be96617 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/templates/databases/sql/default/delete.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/templates/databases/sql/default/delete.sql @@ -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 %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/templates/databases/sql/default/delete_multiple.sql b/web/pgadmin/browser/server_groups/servers/databases/templates/databases/sql/default/delete_multiple.sql deleted file mode 100644 index a39d81bdc..000000000 --- a/web/pgadmin/browser/server_groups/servers/databases/templates/databases/sql/default/delete_multiple.sql +++ /dev/null @@ -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 %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/tests/test_db_delete_multiple_force.py b/web/pgadmin/browser/server_groups/servers/databases/tests/test_db_delete_multiple_force.py new file mode 100644 index 000000000..2ab458442 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/tests/test_db_delete_multiple_force.py @@ -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) diff --git a/web/pgadmin/browser/static/js/keyboard.js b/web/pgadmin/browser/static/js/keyboard.js index 97c44f55b..996d870b2 100644 --- a/web/pgadmin/browser/static/js/keyboard.js +++ b/web/pgadmin/browser/static/js/keyboard.js @@ -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 }; diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js index cdd32d794..5742dedb7 100644 --- a/web/pgadmin/browser/static/js/node.js +++ b/web/pgadmin/browser/static/js/node.js @@ -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) ? diff --git a/web/pgadmin/misc/properties/CollectionNodeProperties.jsx b/web/pgadmin/misc/properties/CollectionNodeProperties.jsx index 5d221f4f7..33530e29c 100644 --- a/web/pgadmin/misc/properties/CollectionNodeProperties.jsx +++ b/web/pgadmin/misc/properties/CollectionNodeProperties.jsx @@ -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 ( } - 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 } > - } - 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 } - > + > : + } + aria-label="Delete Force" + title={gettext('Delete (Force)')} + onClick={() => { + onDrop('dropForce'); + }} + disabled={ + (selectedObject.length > 0) + ? !canDropForce + : true + } + >} ); }; return ( + {data.length > 0 ? ( diff --git a/web/pgadmin/static/js/utils.js b/web/pgadmin/static/js/utils.js index d5a89b6a4..d017b5c62 100644 --- a/web/pgadmin/static/js/utils.js +++ b/web/pgadmin/static/js/utils.js @@ -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; } diff --git a/web/regression/python_test_utils/test_utils.py b/web/regression/python_test_utils/test_utils.py index c08183e52..f8a392c2b 100644 --- a/web/regression/python_test_utils/test_utils.py +++ b/web/regression/python_test_utils/test_utils.py @@ -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()