Commit 0e460e8d authored by Benjamin Eberlei's avatar Benjamin Eberlei

Merge branch 'DBAL-55'

parents 3d55dc8e 1b3e0d3b
...@@ -101,6 +101,13 @@ class Connection implements DriverConnection ...@@ -101,6 +101,13 @@ class Connection implements DriverConnection
*/ */
private $_transactionIsolationLevel; private $_transactionIsolationLevel;
/**
* If nested transations should use savepoints
*
* @var integer
*/
private $_nestTransactionsWithSavepoints;
/** /**
* The parameters used during creation of the Connection instance. * The parameters used during creation of the Connection instance.
* *
...@@ -740,6 +747,48 @@ class Connection implements DriverConnection ...@@ -740,6 +747,48 @@ class Connection implements DriverConnection
} }
} }
/**
* Set if nested transactions should use savepoints
*
* @param boolean
*/
public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
{
if ($this->_transactionNestingLevel > 0) {
throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction();
}
if ($nestTransactionsWithSavepoints && !$this->_platform->supportsSavepoints()) {
ConnectionException::savepointsNotSupported();
}
$this->_nestTransactionsWithSavepoints = $nestTransactionsWithSavepoints;
}
/**
* Get if nested transactions should use savepoints
*
* @return boolean
*/
public function getNestTransactionsWithSavepoints()
{
return $this->_nestTransactionsWithSavepoints;
}
/**
* Returns the savepoint name to use for nested transactions are false if they are not supported
* "savepointFormat" parameter is not set
*
* @return mixed a string with the savepoint name or false
*/
protected function _getNestedTransactionSavePointName() {
if ($this->_platform->supportsSavepoints() && !empty($this->_nestTransactionsWithSavepoints)) {
return 'DOCTRINE2_SAVEPOINT_'.$this->_transactionNestingLevel;
}
return false;
}
/** /**
* Starts a transaction by suspending auto-commit mode. * Starts a transaction by suspending auto-commit mode.
* *
...@@ -749,11 +798,16 @@ class Connection implements DriverConnection ...@@ -749,11 +798,16 @@ class Connection implements DriverConnection
{ {
$this->connect(); $this->connect();
if ($this->_transactionNestingLevel == 0) { ++$this->_transactionNestingLevel;
if ($this->_transactionNestingLevel == 1) {
$this->_conn->beginTransaction(); $this->_conn->beginTransaction();
} else {
$savepointName = $this->_getNestedTransactionSavePointName();
if ($savepointName) {
$this->createSavePoint($savepointName);
}
} }
++$this->_transactionNestingLevel;
} }
/** /**
...@@ -776,6 +830,11 @@ class Connection implements DriverConnection ...@@ -776,6 +830,11 @@ class Connection implements DriverConnection
if ($this->_transactionNestingLevel == 1) { if ($this->_transactionNestingLevel == 1) {
$this->_conn->commit(); $this->_conn->commit();
} else {
$savepointName = $this->_getNestedTransactionSavePointName();
if ($savepointName && $this->_platform->supportsReleaseSavepoints()) {
$this->releaseSavePoint($savepointName);
}
} }
--$this->_transactionNestingLevel; --$this->_transactionNestingLevel;
...@@ -802,11 +861,66 @@ class Connection implements DriverConnection ...@@ -802,11 +861,66 @@ class Connection implements DriverConnection
$this->_conn->rollback(); $this->_conn->rollback();
$this->_isRollbackOnly = false; $this->_isRollbackOnly = false;
} else { } else {
$this->_isRollbackOnly = true; $savepointName = $this->_getNestedTransactionSavePointName();
if (!$this->_isRollbackOnly && $savepointName) {
$this->rollbackSavePoint($savepointName);
} else {
$this->_isRollbackOnly = true;
}
--$this->_transactionNestingLevel; --$this->_transactionNestingLevel;
} }
} }
/**
* createSavepoint
* creates a new savepoint
*
* @param string $savepoint name of a savepoint to set
* @return void
*/
public function createSavePoint($savepoint)
{
if (!$this->_platform->supportsSavepoints()) {
ConnectionException::savepointsNotSupported();
}
$this->_conn->exec($this->_platform->createSavePoint($savepoint));
}
/**
* releaseSavePoint
* releases given savepoint
*
* @param string $savepoint name of a savepoint to release
* @return void
*/
public function releaseSavePoint($savepoint)
{
if (!$this->_platform->supportsSavepoints()) {
ConnectionException::savepointsNotSupported();
}
if ($this->_platform->supportsReleaseSavepoints()) {
$this->_conn->exec($this->_platform->releaseSavePoint($savepoint));
}
}
/**
* rollbackSavePoint
* releases given savepoint
*
* @param string $savepoint name of a savepoint to rollback to
* @return void
*/
public function rollbackSavePoint($savepoint)
{
if (!$this->_platform->supportsSavepoints()) {
ConnectionException::savepointsNotSupported();
}
$this->_conn->exec($this->_platform->rollbackSavePoint($savepoint));
}
/** /**
* Gets the wrapped driver connection. * Gets the wrapped driver connection.
* *
......
...@@ -41,4 +41,14 @@ class ConnectionException extends DBALException ...@@ -41,4 +41,14 @@ class ConnectionException extends DBALException
{ {
return new self("There is no active transaction."); return new self("There is no active transaction.");
} }
public static function savepointsNotSupported()
{
return new self("Savepoints are not supported transaction.");
}
public static function mayNotAlterNestedTransactionWithSavepointsInTransaction()
{
return new self("May not alter the nested transaction with savepoints behavior while a transaction is open.");
}
} }
\ No newline at end of file
...@@ -1773,6 +1773,16 @@ abstract class AbstractPlatform ...@@ -1773,6 +1773,16 @@ abstract class AbstractPlatform
return true; return true;
} }
/**
* Whether the platform supports releasing savepoints.
*
* @return boolean
*/
public function supportsReleaseSavepoints()
{
return $this->supportsSavepoints();
}
/** /**
* Whether the platform supports primary key constraints. * Whether the platform supports primary key constraints.
* *
...@@ -1986,4 +1996,37 @@ abstract class AbstractPlatform ...@@ -1986,4 +1996,37 @@ abstract class AbstractPlatform
{ {
return 'SELECT 1'; return 'SELECT 1';
} }
/**
* Generate SQL to create a new savepoint
*
* @param string $savepoint
* @return string
*/
public function createSavePoint($savepoint)
{
return 'SAVEPOINT ' . $savepoint;
}
/**
* Generate SQL to release a savepoint
*
* @param string $savepoint
* @return string
*/
public function releaseSavePoint($savepoint)
{
return 'RELEASE SAVEPOINT ' . $savepoint;
}
/**
* Generate SQL to rollback a savepoint
*
* @param string $savepoint
* @return string
*/
public function rollbackSavePoint($savepoint)
{
return 'ROLLBACK TO SAVEPOINT ' . $savepoint;
}
} }
\ No newline at end of file
...@@ -291,6 +291,16 @@ class DB2Platform extends AbstractPlatform ...@@ -291,6 +291,16 @@ class DB2Platform extends AbstractPlatform
return false; return false;
} }
/**
* Whether the platform supports releasing savepoints.
*
* @return boolean
*/
public function supportsReleaseSavepoints()
{
return false;
}
/** /**
* Gets the SQL specific for the platform to get the current date. * Gets the SQL specific for the platform to get the current date.
* *
...@@ -539,4 +549,16 @@ class DB2Platform extends AbstractPlatform ...@@ -539,4 +549,16 @@ class DB2Platform extends AbstractPlatform
{ {
return 'SELECT 1 FROM sysibm.sysdummy1'; return 'SELECT 1 FROM sysibm.sysdummy1';
} }
/**
* DB2 supports savepoints, but they work semantically different than on other vendor platforms.
*
* TODO: We have to investigate how to get DB2 up and running with savepoints.
*
* @return bool
*/
public function supportsSavepoints()
{
return false;
}
} }
\ No newline at end of file
...@@ -61,12 +61,11 @@ class MsSqlPlatform extends AbstractPlatform ...@@ -61,12 +61,11 @@ class MsSqlPlatform extends AbstractPlatform
} }
/** /**
* Whether the platform supports savepoints. MsSql does not. * Whether the platform supports releasing savepoints.
* *
* @return boolean * @return boolean
* @override
*/ */
public function supportsSavepoints() public function supportsReleaseSavepoints()
{ {
return false; return false;
} }
...@@ -92,7 +91,7 @@ class MsSqlPlatform extends AbstractPlatform ...@@ -92,7 +91,7 @@ class MsSqlPlatform extends AbstractPlatform
*/ */
public function getDropDatabaseSQL($name) public function getDropDatabaseSQL($name)
{ {
return 'ALTER DATABASE [' . $name . '] return 'ALTER DATABASE [' . $name . ']
SET SINGLE_USER --or RESTRICTED_USER SET SINGLE_USER --or RESTRICTED_USER
WITH ROLLBACK IMMEDIATE; WITH ROLLBACK IMMEDIATE;
DROP DATABASE ' . $name . ';'; DROP DATABASE ' . $name . ';';
...@@ -121,37 +120,36 @@ DROP DATABASE ' . $name . ';'; ...@@ -121,37 +120,36 @@ DROP DATABASE ' . $name . ';';
return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey; return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
} }
/** /**
* @override * @override
*/ */
public function getDropIndexSQL($index, $table=null) public function getDropIndexSQL($index, $table=null)
{ {
if($index instanceof \Doctrine\DBAL\Schema\Index) { if($index instanceof \Doctrine\DBAL\Schema\Index) {
$index_ = $index;
$index = $index->getName(); $index = $index->getName();
} else if(!is_string($index)) { } else if(!is_string($index)) {
throw new \InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.'); throw new \InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
} }
if (!isset($table)) { if (!isset($table)) {
return 'DROP INDEX ' . $index; return 'DROP INDEX ' . $index;
} else { } else {
if ($table instanceof \Doctrine\DBAL\Schema\Table) { if ($table instanceof \Doctrine\DBAL\Schema\Table) {
$table = $table->getName(); $table = $table->getName();
} }
return "IF EXISTS (SELECT * FROM sysobjects WHERE name = '$index') return "IF EXISTS (SELECT * FROM sysobjects WHERE name = '$index')
ALTER TABLE " . $this->quoteIdentifier($table) . " DROP CONSTRAINT " . $this->quoteIdentifier($index) . " ALTER TABLE " . $this->quoteIdentifier($table) . " DROP CONSTRAINT " . $this->quoteIdentifier($index) . "
ELSE ELSE
DROP INDEX " . $this->quoteIdentifier($index) . " ON " . $this->quoteIdentifier($table); DROP INDEX " . $this->quoteIdentifier($index) . " ON " . $this->quoteIdentifier($table);
} }
} }
/** /**
* @override * @override
*/ */
protected function _getCreateTableSQL($tableName, array $columns, array $options = array()) protected function _getCreateTableSQL($tableName, array $columns, array $options = array())
{ {
$columnListSql = $this->getColumnDeclarationListSQL($columns); $columnListSql = $this->getColumnDeclarationListSQL($columns);
...@@ -174,8 +172,8 @@ DROP DATABASE ' . $name . ';'; ...@@ -174,8 +172,8 @@ DROP DATABASE ' . $name . ';';
$query .= ')'; $query .= ')';
$sql[] = $query; $sql[] = $query;
if (isset($options['indexes']) && ! empty($options['indexes'])) { if (isset($options['indexes']) && ! empty($options['indexes'])) {
foreach ($options['indexes'] AS $index) { foreach ($options['indexes'] AS $index) {
$sql[] = $this->getCreateIndexSQL($index, $tableName); $sql[] = $this->getCreateIndexSQL($index, $tableName);
} }
...@@ -222,9 +220,9 @@ DROP DATABASE ' . $name . ';'; ...@@ -222,9 +220,9 @@ DROP DATABASE ' . $name . ';';
$sql = array(); $sql = array();
foreach ($queryParts as $query) { foreach ($queryParts as $query) {
$sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query; $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query;
} }
$sql = array_merge($sql, $this->_getAlterTableIndexForeignKeySQL($diff)); $sql = array_merge($sql, $this->_getAlterTableIndexForeignKeySQL($diff));
...@@ -291,24 +289,24 @@ DROP DATABASE ' . $name . ';'; ...@@ -291,24 +289,24 @@ DROP DATABASE ' . $name . ';';
{ {
return "exec sp_helpindex '" . $table . "'"; return "exec sp_helpindex '" . $table . "'";
} }
/** /**
* @override * @override
*/ */
public function getCreateViewSQL($name, $sql) public function getCreateViewSQL($name, $sql)
{ {
return 'CREATE VIEW ' . $name . ' AS ' . $sql; return 'CREATE VIEW ' . $name . ' AS ' . $sql;
} }
/** /**
* @override * @override
*/ */
public function getListViewsSQL($database) public function getListViewsSQL($database)
{ {
return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name"; return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
} }
/** /**
* @override * @override
*/ */
public function getDropViewSQL($name) public function getDropViewSQL($name)
...@@ -386,8 +384,8 @@ DROP DATABASE ' . $name . ';'; ...@@ -386,8 +384,8 @@ DROP DATABASE ' . $name . ';';
$args = func_get_args(); $args = func_get_args();
return '(' . implode(' + ', $args) . ')'; return '(' . implode(' + ', $args) . ')';
} }
public function getListDatabasesSQL() public function getListDatabasesSQL()
{ {
return 'SELECT * FROM SYS.DATABASES'; return 'SELECT * FROM SYS.DATABASES';
} }
...@@ -644,7 +642,7 @@ DROP DATABASE ' . $name . ';'; ...@@ -644,7 +642,7 @@ DROP DATABASE ' . $name . ';';
{ {
return 'Y-m-d H:i:s.u'; return 'Y-m-d H:i:s.u';
} }
/** /**
* @override * @override
*/ */
...@@ -667,4 +665,37 @@ DROP DATABASE ' . $name . ';'; ...@@ -667,4 +665,37 @@ DROP DATABASE ' . $name . ';';
{ {
} }
/**
* Generate SQL to create a new savepoint
*
* @param string $savepoint
* @return string
*/
public function createSavePoint($savepoint)
{
return 'SAVE TRANSACTION ' . $savepoint;
}
/**
* Generate SQL to release a savepoint
*
* @param string $savepoint
* @return string
*/
public function releaseSavePoint($savepoint)
{
return '';
}
/**
* Generate SQL to rollback a savepoint
*
* @param string $savepoint
* @return string
*/
public function rollbackSavePoint($savepoint)
{
return 'ROLLBACK TRANSACTION ' . $savepoint;
}
} }
...@@ -258,17 +258,6 @@ class MySqlPlatform extends AbstractPlatform ...@@ -258,17 +258,6 @@ class MySqlPlatform extends AbstractPlatform
{ {
return true; return true;
} }
/**
* Whether the platform supports savepoints. MySql does not.
*
* @return boolean
* @override
*/
public function supportsSavepoints()
{
return false;
}
public function getShowDatabasesSQL() public function getShowDatabasesSQL()
{ {
......
...@@ -659,6 +659,16 @@ LEFT JOIN all_cons_columns r_cols ...@@ -659,6 +659,16 @@ LEFT JOIN all_cons_columns r_cols
return false; return false;
} }
/**
* Whether the platform supports releasing savepoints.
*
* @return boolean
*/
public function supportsReleaseSavepoints()
{
return false;
}
/** /**
* @inheritdoc * @inheritdoc
*/ */
...@@ -700,4 +710,15 @@ LEFT JOIN all_cons_columns r_cols ...@@ -700,4 +710,15 @@ LEFT JOIN all_cons_columns r_cols
'urowid' => 'string' 'urowid' => 'string'
); );
} }
/**
* Generate SQL to release a savepoint
*
* @param string $savepoint
* @return string
*/
public function releaseSavePoint($savepoint)
{
return '';
}
} }
...@@ -44,7 +44,7 @@ class ConnectionTest extends \Doctrine\Tests\DbalFunctionalTestCase ...@@ -44,7 +44,7 @@ class ConnectionTest extends \Doctrine\Tests\DbalFunctionalTestCase
//no rethrow //no rethrow
} }
$this->assertTrue($this->_conn->isRollbackOnly()); $this->assertTrue($this->_conn->isRollbackOnly());
$this->_conn->commit(); // should throw exception $this->_conn->commit(); // should throw exception
$this->fail('Transaction commit after failed nested transaction should fail.'); $this->fail('Transaction commit after failed nested transaction should fail.');
} catch (ConnectionException $e) { } catch (ConnectionException $e) {
...@@ -53,7 +53,46 @@ class ConnectionTest extends \Doctrine\Tests\DbalFunctionalTestCase ...@@ -53,7 +53,46 @@ class ConnectionTest extends \Doctrine\Tests\DbalFunctionalTestCase
$this->assertEquals(0, $this->_conn->getTransactionNestingLevel()); $this->assertEquals(0, $this->_conn->getTransactionNestingLevel());
} }
} }
public function testTransactionNestingBehaviorWithSavepoints()
{
if (!$this->_conn->getDatabasePlatform()->supportsSavepoints()) {
$this->markTestSkipped('This test requires the platform to support savepoints.');
}
$this->_conn->setNestTransactionsWithSavepoints(true);
try {
$this->_conn->beginTransaction();
$this->assertEquals(1, $this->_conn->getTransactionNestingLevel());
try {
$this->_conn->beginTransaction();
$this->assertEquals(2, $this->_conn->getTransactionNestingLevel());
$this->_conn->beginTransaction();
$this->assertEquals(3, $this->_conn->getTransactionNestingLevel());
$this->_conn->commit();
$this->assertEquals(2, $this->_conn->getTransactionNestingLevel());
throw new \Exception;
$this->_conn->commit(); // never reached
} catch (\Exception $e) {
$this->_conn->rollback();
$this->assertEquals(1, $this->_conn->getTransactionNestingLevel());
//no rethrow
}
$this->assertFalse($this->_conn->isRollbackOnly());
try {
$this->_conn->setNestTransactionsWithSavepoints(false);
$this->fail('Should not be able to disable savepoints in usage for nested transactions inside an open transaction.');
} catch (ConnectionException $e) {
$this->assertTrue($this->_conn->getNestTransactionsWithSavepoints());
}
$this->_conn->commit(); // should not throw exception
} catch (ConnectionException $e) {
$this->fail('Transaction commit after failed nested transaction should not fail when using savepoints.');
$this->_conn->rollback();
}
}
public function testTransactionBehaviorWithRollback() public function testTransactionBehaviorWithRollback()
{ {
try { try {
......
...@@ -23,14 +23,14 @@ class MsSqlPlatformTest extends AbstractPlatformTestCase ...@@ -23,14 +23,14 @@ class MsSqlPlatformTest extends AbstractPlatformTestCase
{ {
return array( return array(
'CREATE TABLE test (foo VARCHAR(255) DEFAULT NULL, bar VARCHAR(255) DEFAULT NULL)', 'CREATE TABLE test (foo VARCHAR(255) DEFAULT NULL, bar VARCHAR(255) DEFAULT NULL)',
'CREATE UNIQUE INDEX test_foo_bar_uniq ON test (foo, bar)' 'CREATE UNIQUE INDEX test_foo_bar_uniq ON test (foo, bar)'
); );
} }
public function getGenerateAlterTableSql() public function getGenerateAlterTableSql()
{ {
return array( return array(
'ALTER TABLE mytable RENAME TO userlist', 'ALTER TABLE mytable RENAME TO userlist',
'ALTER TABLE mytable ADD quota INT DEFAULT NULL', 'ALTER TABLE mytable ADD quota INT DEFAULT NULL',
'ALTER TABLE mytable DROP COLUMN foo', 'ALTER TABLE mytable DROP COLUMN foo',
'ALTER TABLE mytable CHANGE bar baz VARCHAR(255) DEFAULT \'def\' NOT NULL', 'ALTER TABLE mytable CHANGE bar baz VARCHAR(255) DEFAULT \'def\' NOT NULL',
...@@ -127,7 +127,7 @@ DDB; ...@@ -127,7 +127,7 @@ DDB;
public function testDoesNotSupportSavePoints() public function testDoesNotSupportSavePoints()
{ {
$this->assertFalse($this->_platform->supportsSavepoints()); $this->assertTrue($this->_platform->supportsSavepoints());
} }
public function getGenerateIndexSql() public function getGenerateIndexSql()
......
...@@ -125,9 +125,9 @@ class MySqlPlatformTest extends AbstractPlatformTestCase ...@@ -125,9 +125,9 @@ class MySqlPlatformTest extends AbstractPlatformTestCase
$this->assertTrue($this->_platform->supportsIdentityColumns()); $this->assertTrue($this->_platform->supportsIdentityColumns());
} }
public function testDoesNotSupportSavePoints() public function testDoesSupportSavePoints()
{ {
$this->assertFalse($this->_platform->supportsSavepoints()); $this->assertTrue($this->_platform->supportsSavepoints());
} }
public function getGenerateIndexSql() public function getGenerateIndexSql()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment