diff --git a/src/commands/database.js b/src/commands/database.js index 22c2d59a4..a7fc81bbe 100644 --- a/src/commands/database.js +++ b/src/commands/database.js @@ -29,6 +29,11 @@ exports.builder = (yargs) => .option('template', { describe: 'Pass template option to dialect, PostgreSQL only', type: 'string', + }) + .option('force', { + describe: + 'Pass force option to dialect with db:drop, PostgreSQL > v13 only', + type: 'boolean', }).argv; exports.handler = async function (args) { @@ -45,18 +50,16 @@ exports.handler = async function (args) { 'encoding', 'ctype', 'template', + 'force', ]); - const queryInterface = sequelize.getQueryInterface(); - const queryGenerator = - queryInterface.queryGenerator || queryInterface.QueryGenerator; - - const query = getCreateDatabaseQuery(sequelize, config, options); + const createQuery = getCreateDatabaseQuery(sequelize, config, options); + const dropQuery = await getDropDatabaseQuery(sequelize, config, options); switch (command) { case 'db:create': await sequelize - .query(query, { + .query(createQuery, { type: sequelize.QueryTypes.RAW, }) .catch((e) => helpers.view.error(e)); @@ -66,14 +69,9 @@ exports.handler = async function (args) { break; case 'db:drop': await sequelize - .query( - `DROP DATABASE IF EXISTS ${queryGenerator.quoteIdentifier( - config.database - )}`, - { - type: sequelize.QueryTypes.RAW, - } - ) + .query(dropQuery, { + type: sequelize.QueryTypes.RAW, + }) .catch((e) => helpers.view.error(e)); helpers.view.log('Database', clc.blueBright(config.database), 'dropped.'); @@ -84,6 +82,13 @@ exports.handler = async function (args) { process.exit(0); }; +/** + * + * @param sequelize + * @param config + * @param options + * @returns {string} + */ function getCreateDatabaseQuery(sequelize, config, options) { const queryInterface = sequelize.getQueryInterface(); const queryGenerator = @@ -145,6 +150,74 @@ function getCreateDatabaseQuery(sequelize, config, options) { } } +/** + * + * @param sequelize + * @returns {Promise} + */ +async function getPostgresVersion(sequelize) { + try { + const [results] = await sequelize.query( + `SELECT current_setting('server_version_num')::int AS version`, + { + type: sequelize.QueryTypes.RAW, + } + ); + return results[0]?.version ?? null; + } catch (e) { + return null; + } +} + +/** + * + * @param sequelize + * @param config + * @param options + * @returns {Promise} + */ +async function getDropDatabaseQuery(sequelize, config, options) { + // Adds the force option for WITH(FORCE) to drop a database that has connected users, fallback to default drop if version lower + // for postgres v13 and above see manual https://www.postgresql.org/docs/current/sql-dropdatabase.html + + const POSTGRES_FORCE_DROP_MIN_VERSION = 130000; + const queryInterface = sequelize.getQueryInterface(); + const queryGenerator = + queryInterface.queryGenerator || queryInterface.QueryGenerator; + + switch (config.dialect) { + case 'postgres': + if (options.force) { + const version = await getPostgresVersion(sequelize); + helpers.view.log( + clc.redBright( + `WARNING :: Force dropping database, version check ${ + version < POSTGRES_FORCE_DROP_MIN_VERSION + ? 'NOT OK!. will ignore --force flag' + : 'OK!. will force drop database regardless of connected users' + } ` + ) + ); + + return `DROP DATABASE IF EXISTS ${queryGenerator.quoteIdentifier( + config.database + )} ${ + Number(version) >= POSTGRES_FORCE_DROP_MIN_VERSION + ? 'WITH (FORCE)' + : '' + } ;`; + } else + return `DROP DATABASE IF EXISTS ${queryGenerator.quoteIdentifier( + config.database + )}`; + + default: + return `DROP DATABASE IF EXISTS ${queryGenerator.quoteIdentifier( + config.database + )}`; + } +} + function getDatabaseLessSequelize() { let config = null; diff --git a/test/db/db-drop.test.js b/test/db/db-drop.test.js index 832d02136..f7b785e72 100644 --- a/test/db/db-drop.test.js +++ b/test/db/db-drop.test.js @@ -48,6 +48,30 @@ describe(Support.getTestDialectTeaser('db:drop'), () => { } ); }); + it('correctly drops database with force', function (done) { + const databaseName = `my_test_db_${_.random(10000, 99999)}`; + prepare( + 'db:drop --force', + () => { + this.sequelize + .query( + `SELECT 1 as exists FROM pg_database WHERE datname = '${databaseName}';`, + { + type: this.sequelize.QueryTypes.SELECT, + } + ) + .then((result) => { + expect(result).to.be.empty; + done(); + }); + }, + { + config: { + database: databaseName, + }, + } + ); + }); } if (Support.dialectIsMySQL()) {