From 984bc547acb7fbe8d4cf649606f4b1f416b4fdd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20M=C3=BCller?= Date: Fri, 1 May 2015 10:16:55 +0200 Subject: [PATCH] fix dropping database with active connection on PostgreSQL --- .../DBAL/Platforms/PostgreSQL92Platform.php | 8 +++++ .../DBAL/Platforms/PostgreSqlPlatform.php | 28 +++++++++++++++++ .../DBAL/Schema/PostgreSqlSchemaManager.php | 28 +++++++++++++++++ .../SchemaManagerFunctionalTestCase.php | 30 +++++++++++++++++++ .../Schema/SqliteSchemaManagerTest.php | 26 ++++++++++++++++ .../AbstractPostgreSqlPlatformTestCase.php | 22 ++++++++++++++ .../Platforms/PostgreSQL92PlatformTest.php | 11 +++++++ 7 files changed, 153 insertions(+) diff --git a/lib/Doctrine/DBAL/Platforms/PostgreSQL92Platform.php b/lib/Doctrine/DBAL/Platforms/PostgreSQL92Platform.php index dcb5ebe9c7f..19cf8035630 100644 --- a/lib/Doctrine/DBAL/Platforms/PostgreSQL92Platform.php +++ b/lib/Doctrine/DBAL/Platforms/PostgreSQL92Platform.php @@ -72,4 +72,12 @@ protected function initializeDoctrineTypeMappings() parent::initializeDoctrineTypeMappings(); $this->doctrineTypeMapping['json'] = 'json_array'; } + + /** + * {@inheritdoc} + */ + public function getCloseActiveDatabaseConnectionsSQL($database) + { + return "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$database'"; + } } diff --git a/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php b/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php index cb512c47939..575e144bda6 100644 --- a/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php @@ -420,6 +420,34 @@ public function getCreateDatabaseSQL($name) return 'CREATE DATABASE ' . $name; } + /** + * Returns the SQL statement for disallowing new connections on the given database. + * + * This is useful to force DROP DATABASE operations which could fail because of active connections. + * + * @param string $database The name of the database to disallow new connections for. + * + * @return string + */ + public function getDisallowDatabaseConnectionsSQL($database) + { + return "UPDATE pg_database SET datallowconn = 'false' WHERE datname = '$database'"; + } + + /** + * Returns the SQL statement for closing currently active connections on the given database. + * + * This is useful to force DROP DATABASE operations which could fail because of active connections. + * + * @param string $database The name of the database to close currently active connections for. + * + * @return string + */ + public function getCloseActiveDatabaseConnectionsSQL($database) + { + return "SELECT pg_terminate_backend(procpid) FROM pg_stat_activity WHERE datname = '$database'"; + } + /** * {@inheritDoc} */ diff --git a/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php b/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php index ee2606d6a53..5b7b82e152b 100644 --- a/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php +++ b/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php @@ -19,6 +19,7 @@ namespace Doctrine\DBAL\Schema; +use Doctrine\DBAL\Exception\DriverException; use Doctrine\DBAL\Types\Type; /** @@ -100,6 +101,33 @@ public function determineExistingSchemaSearchPaths() }); } + /** + * {@inheritdoc} + */ + public function dropDatabase($database) + { + try { + parent::dropDatabase($database); + } catch (DriverException $exception) { + // If we have a SQLSTATE 55006, the drop database operation failed + // because of active connections on the database. + // To force dropping the database, we first have to close all active connections + // on that database and issue the drop database operation again. + if ($exception->getSQLState() !== '55006') { + throw $exception; + } + + $this->_execSql( + array( + $this->_platform->getDisallowDatabaseConnectionsSQL($database), + $this->_platform->getCloseActiveDatabaseConnectionsSQL($database), + ) + ); + + parent::dropDatabase($database); + } + } + /** * {@inheritdoc} */ diff --git a/tests/Doctrine/Tests/DBAL/Functional/Schema/SchemaManagerFunctionalTestCase.php b/tests/Doctrine/Tests/DBAL/Functional/Schema/SchemaManagerFunctionalTestCase.php index a2cf7311d6d..3a9bf61b294 100644 --- a/tests/Doctrine/Tests/DBAL/Functional/Schema/SchemaManagerFunctionalTestCase.php +++ b/tests/Doctrine/Tests/DBAL/Functional/Schema/SchemaManagerFunctionalTestCase.php @@ -40,6 +40,36 @@ protected function setUp() $this->_sm = $this->_conn->getSchemaManager(); } + /** + * @group DBAL-1220 + */ + public function testDropsDatabaseWithActiveConnections() + { + if (! $this->_sm->getDatabasePlatform()->supportsCreateDropDatabase()) { + $this->markTestSkipped('Cannot drop Database client side with this Driver.'); + } + + $this->_sm->dropAndCreateDatabase('test_drop_database'); + + $this->assertContains('test_drop_database', $this->_sm->listDatabases()); + + $params = $this->_conn->getParams(); + $params['dbname'] = 'test_drop_database'; + + $user = isset($params['user']) ? $params['user'] : null; + $password = isset($params['password']) ? $params['password'] : null; + + $connection = $this->_conn->getDriver()->connect($params, $user, $password); + + $this->assertInstanceOf('Doctrine\DBAL\Driver\Connection', $connection); + + $this->_sm->dropDatabase('test_drop_database'); + + $this->assertNotContains('test_drop_database', $this->_sm->listDatabases()); + + unset($connection); + } + /** * @group DBAL-195 */ diff --git a/tests/Doctrine/Tests/DBAL/Functional/Schema/SqliteSchemaManagerTest.php b/tests/Doctrine/Tests/DBAL/Functional/Schema/SqliteSchemaManagerTest.php index 67282b1c204..dd03a0f9877 100644 --- a/tests/Doctrine/Tests/DBAL/Functional/Schema/SqliteSchemaManagerTest.php +++ b/tests/Doctrine/Tests/DBAL/Functional/Schema/SqliteSchemaManagerTest.php @@ -26,6 +26,32 @@ public function testCreateAndDropDatabase() $this->assertEquals(false, file_exists($path)); } + /** + * @group DBAL-1220 + */ + public function testDropsDatabaseWithActiveConnections() + { + $this->_sm->dropAndCreateDatabase('test_drop_database'); + + $this->assertFileExists('test_drop_database'); + + $params = $this->_conn->getParams(); + $params['dbname'] = 'test_drop_database'; + + $user = isset($params['user']) ? $params['user'] : null; + $password = isset($params['password']) ? $params['password'] : null; + + $connection = $this->_conn->getDriver()->connect($params, $user, $password); + + $this->assertInstanceOf('Doctrine\DBAL\Driver\Connection', $connection); + + $this->_sm->dropDatabase('test_drop_database'); + + $this->assertFileNotExists('test_drop_database'); + + unset($connection); + } + public function testRenameTable() { $this->createTestTable('oldname'); diff --git a/tests/Doctrine/Tests/DBAL/Platforms/AbstractPostgreSqlPlatformTestCase.php b/tests/Doctrine/Tests/DBAL/Platforms/AbstractPostgreSqlPlatformTestCase.php index 58da19b7397..29295c25a08 100644 --- a/tests/Doctrine/Tests/DBAL/Platforms/AbstractPostgreSqlPlatformTestCase.php +++ b/tests/Doctrine/Tests/DBAL/Platforms/AbstractPostgreSqlPlatformTestCase.php @@ -792,4 +792,26 @@ public function testInitializesTsvectorTypeMapping() $this->assertTrue($this->_platform->hasDoctrineTypeMappingFor('tsvector')); $this->assertEquals('text', $this->_platform->getDoctrineTypeMapping('tsvector')); } + + /** + * @group DBAL-1220 + */ + public function testReturnsDisallowDatabaseConnectionsSQL() + { + $this->assertSame( + "UPDATE pg_database SET datallowconn = 'false' WHERE datname = 'foo'", + $this->_platform->getDisallowDatabaseConnectionsSQL('foo') + ); + } + + /** + * @group DBAL-1220 + */ + public function testReturnsCloseActiveDatabaseConnectionsSQL() + { + $this->assertSame( + "SELECT pg_terminate_backend(procpid) FROM pg_stat_activity WHERE datname = 'foo'", + $this->_platform->getCloseActiveDatabaseConnectionsSQL('foo') + ); + } } diff --git a/tests/Doctrine/Tests/DBAL/Platforms/PostgreSQL92PlatformTest.php b/tests/Doctrine/Tests/DBAL/Platforms/PostgreSQL92PlatformTest.php index 083df4beead..2ec361242b1 100644 --- a/tests/Doctrine/Tests/DBAL/Platforms/PostgreSQL92PlatformTest.php +++ b/tests/Doctrine/Tests/DBAL/Platforms/PostgreSQL92PlatformTest.php @@ -56,4 +56,15 @@ public function testInitializesJsonTypeMapping() $this->assertTrue($this->_platform->hasDoctrineTypeMappingFor('json')); $this->assertEquals('json_array', $this->_platform->getDoctrineTypeMapping('json')); } + + /** + * @group DBAL-1220 + */ + public function testReturnsCloseActiveDatabaseConnectionsSQL() + { + $this->assertSame( + "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'foo'", + $this->_platform->getCloseActiveDatabaseConnectionsSQL('foo') + ); + } }