diff --git a/lib/Cake/Model/Datasource/Database/Mysql.php b/lib/Cake/Model/Datasource/Database/Mysql.php index b5421f43d..70c51366c 100644 --- a/lib/Cake/Model/Datasource/Database/Mysql.php +++ b/lib/Cake/Model/Datasource/Database/Mysql.php @@ -77,17 +77,6 @@ class Mysql extends DboSource { */ protected $_useAlias = true; -/** - * Index of basic SQL commands - * - * @var array - */ - protected $_commands = array( - 'begin' => 'START TRANSACTION', - 'commit' => 'COMMIT', - 'rollback' => 'ROLLBACK' - ); - /** * List of engine specific additional field parameters used on table creating * @@ -262,15 +251,6 @@ class Mysql extends DboSource { return $this->_execute('SHOW VARIABLES LIKE ?', array('character_set_client'))->fetchObject()->Value; } -/** - * Gets the version string of the database server - * - * @return string The database encoding - */ - public function getVersion() { - return $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION); - } - /** * Query charset by collation * @@ -696,4 +676,13 @@ class Mysql extends DboSource { return $this->config['database']; } +/** + * Check if the server support nested transactions + * + * @return boolean + */ + public function supportNestedTransaction() { + return $this->nestedTransaction && version_compare($this->getVersion(), '4.1', '>='); + } + } diff --git a/lib/Cake/Model/Datasource/Database/Postgres.php b/lib/Cake/Model/Datasource/Database/Postgres.php index 064a1cca4..35f568aad 100644 --- a/lib/Cake/Model/Datasource/Database/Postgres.php +++ b/lib/Cake/Model/Datasource/Database/Postgres.php @@ -33,17 +33,6 @@ class Postgres extends DboSource { */ public $description = "PostgreSQL DBO Driver"; -/** - * Index of basic SQL commands - * - * @var array - */ - protected $_commands = array( - 'begin' => 'BEGIN', - 'commit' => 'COMMIT', - 'rollback' => 'ROLLBACK' - ); - /** * Base driver configuration settings. Merged with user settings. * @@ -906,4 +895,13 @@ class Postgres extends DboSource { return $this->config['schema']; } +/** + * Check if the server support nested transactions + * + * @return boolean + */ + public function supportNestedTransaction() { + return $this->nestedTransaction && version_compare($this->getVersion(), '8.0', '>='); + } + } diff --git a/lib/Cake/Model/Datasource/Database/Sqlite.php b/lib/Cake/Model/Datasource/Database/Sqlite.php index 59db71dcf..419709892 100644 --- a/lib/Cake/Model/Datasource/Database/Sqlite.php +++ b/lib/Cake/Model/Datasource/Database/Sqlite.php @@ -559,4 +559,13 @@ class Sqlite extends DboSource { return "main"; // Sqlite Datasource does not support multidb } +/** + * Check if the server support nested transactions + * + * @return boolean + */ + public function supportNestedTransaction() { + return $this->nestedTransaction && version_compare($this->getVersion(), '3.6.8', '>='); + } + } diff --git a/lib/Cake/Model/Datasource/Database/Sqlserver.php b/lib/Cake/Model/Datasource/Database/Sqlserver.php index d7882663b..adea9beb1 100644 --- a/lib/Cake/Model/Datasource/Database/Sqlserver.php +++ b/lib/Cake/Model/Datasource/Database/Sqlserver.php @@ -98,31 +98,12 @@ class Sqlserver extends DboSource { 'boolean' => array('name' => 'bit') ); -/** - * Index of basic SQL commands - * - * @var array - */ - protected $_commands = array( - 'begin' => 'BEGIN TRANSACTION', - 'commit' => 'COMMIT', - 'rollback' => 'ROLLBACK' - ); - /** * Magic column name used to provide pagination support for SQLServer 2008 * which lacks proper limit/offset support. */ const ROW_COUNTER = '_cake_page_rownum_'; -/** - * The version of SQLServer being used. If greater than 11 - * Normal limit offset statements will be used - * - * @var string - */ - protected $_version; - /** * Connects to the database using options in the given configuration array. * @@ -151,7 +132,6 @@ class Sqlserver extends DboSource { throw new MissingConnectionException(array('class' => $e->getMessage())); } - $this->_version = $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION); return $this->connected; } @@ -515,7 +495,7 @@ class Sqlserver extends DboSource { } // For older versions use the subquery version of pagination. - if (version_compare($this->_version, '11', '<') && preg_match('/FETCH\sFIRST\s+([0-9]+)/i', $limit, $offset)) { + if (version_compare($this->getVersion(), '11', '<') && preg_match('/FETCH\sFIRST\s+([0-9]+)/i', $limit, $offset)) { preg_match('/OFFSET\s*(\d+)\s*.*?(\d+)\s*ROWS/', $limit, $limitOffset); $limit = 'TOP ' . intval($limitOffset[2]); diff --git a/lib/Cake/Model/Datasource/DboSource.php b/lib/Cake/Model/Datasource/DboSource.php index 7e85cc4bb..a37709df9 100644 --- a/lib/Cake/Model/Datasource/DboSource.php +++ b/lib/Cake/Model/Datasource/DboSource.php @@ -69,6 +69,15 @@ class DboSource extends DataSource { */ public $cacheMethods = true; +/** + * Flag to support nested transactions. If it is set to false, you will be able to use + * the transaction methods (begin/commit/rollback), but just the global transaction will + * be executed. + * + * @var boolean + */ + public $nestedTransaction = true; + /** * Print full query debug info? * @@ -183,17 +192,6 @@ class DboSource extends DataSource { */ protected $_transactionNesting = 0; -/** - * Index of basic SQL commands - * - * @var array - */ - protected $_commands = array( - 'begin' => 'BEGIN', - 'commit' => 'COMMIT', - 'rollback' => 'ROLLBACK' - ); - /** * Default fields that are used by the DBO * @@ -294,12 +292,21 @@ class DboSource extends DataSource { /** * Get the underlying connection object. * - * @return PDOConnection + * @return PDO */ public function getConnection() { return $this->_connection; } +/** + * Gets the version string of the database server + * + * @return string The database version + */ + public function getVersion() { + return $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION); + } + /** * Returns a quoted and escaped string of $data for use in an SQL statement. * @@ -2019,6 +2026,15 @@ class DboSource extends DataSource { return $this->execute('TRUNCATE TABLE ' . $this->fullTableName($table)); } +/** + * Check if the server support nested transactions + * + * @return boolean + */ + public function supportNestedTransaction() { + return false; + } + /** * Begin a transaction * @@ -2027,15 +2043,33 @@ class DboSource extends DataSource { * or a transaction has not started). */ public function begin() { - if ($this->_transactionStarted || $this->_connection->beginTransaction()) { - if ($this->fullDebug && empty($this->_transactionNesting)) { - $this->logQuery('BEGIN'); + if ($this->_transactionStarted) { + if ($this->supportNestedTransaction()) { + return $this->_beginNested(); } - $this->_transactionStarted = true; $this->_transactionNesting++; - return true; + return $this->_transactionStarted; } - return false; + + $this->_transactionNesting = 0; + if ($this->fullDebug) { + $this->logQuery('BEGIN'); + } + return $this->_transactionStarted = $this->_connection->beginTransaction(); + } + +/** + * Begin a nested transaction + * + * @return boolean + */ + protected function _beginNested() { + $query = 'SAVEPOINT LEVEL' . ++$this->_transactionNesting; + if ($this->fullDebug) { + $this->logQuery($query); + } + $this->_connection->exec($query); + return true; } /** @@ -2046,19 +2080,38 @@ class DboSource extends DataSource { * or a transaction has not started). */ public function commit() { - if ($this->_transactionStarted) { - $this->_transactionNesting--; - if ($this->_transactionNesting <= 0) { - $this->_transactionStarted = false; - $this->_transactionNesting = 0; - if ($this->fullDebug) { - $this->logQuery('COMMIT'); - } - return $this->_connection->commit(); - } - return true; + if (!$this->_transactionStarted) { + return false; } - return false; + + if ($this->_transactionNesting === 0) { + if ($this->fullDebug) { + $this->logQuery('COMMIT'); + } + $this->_transactionStarted = false; + return $this->_connection->commit(); + } + + if ($this->supportNestedTransaction()) { + return $this->_commitNested(); + } + + $this->_transactionNesting--; + return true; + } + +/** + * Commit a nested transaction + * + * @return boolean + */ + protected function _commitNested() { + $query = 'RELEASE SAVEPOINT LEVEL' . $this->_transactionNesting--; + if ($this->fullDebug) { + $this->logQuery($query); + } + $this->_connection->exec($query); + return true; } /** @@ -2069,15 +2122,38 @@ class DboSource extends DataSource { * or a transaction has not started). */ public function rollback() { - if ($this->_transactionStarted && $this->_connection->rollBack()) { + if (!$this->_transactionStarted) { + return false; + } + + if ($this->_transactionNesting === 0) { if ($this->fullDebug) { $this->logQuery('ROLLBACK'); } $this->_transactionStarted = false; - $this->_transactionNesting = 0; - return true; + return $this->_connection->rollBack(); } - return false; + + if ($this->supportNestedTransaction()) { + return $this->_rollbackNested(); + } + + $this->_transactionNesting--; + return true; + } + +/** + * Rollback a nested transaction + * + * @return boolean + */ + protected function _rollbackNested() { + $query = 'ROLLBACK TO SAVEPOINT LEVEL' . $this->_transactionNesting--; + if ($this->fullDebug) { + $this->logQuery($query); + } + $this->_connection->exec($query); + return true; } /** diff --git a/lib/Cake/Test/Case/Model/Datasource/Database/MysqlTest.php b/lib/Cake/Test/Case/Model/Datasource/Database/MysqlTest.php index 3024a0607..9fa5f9bd4 100644 --- a/lib/Cake/Test/Case/Model/Datasource/Database/MysqlTest.php +++ b/lib/Cake/Test/Case/Model/Datasource/Database/MysqlTest.php @@ -45,7 +45,7 @@ class MysqlTest extends CakeTestCase { public $fixtures = array( 'core.apple', 'core.article', 'core.articles_tag', 'core.attachment', 'core.comment', 'core.sample', 'core.tag', 'core.user', 'core.post', 'core.author', 'core.data_test', - 'core.binary_test' + 'core.binary_test', 'app.address' ); /** @@ -3579,4 +3579,38 @@ class MysqlTest extends CakeTestCase { ->with("TRUNCATE TABLE `$schema`.`tbl_articles`"); $this->Dbo->truncate('articles'); } + +/** + * Test nested transaction + * + * @return void + */ + public function testNestedTransaction() { + $this->skipIf($this->Dbo->supportNestedTransaction() === false, 'The MySQL server do not support nested transaction'); + + $this->loadFixtures('Address'); + $model = ClassRegistry::init('Address'); + $model->hasOne = $model->hasMany = $model->belongsTo = $model->hasAndBelongsToMany = array(); + $model->cacheQueries = false; + $this->Dbo->cacheMethods = false; + + $this->assertTrue($this->Dbo->begin()); + $this->assertNotEmpty($model->read(null, 1)); + + $this->assertTrue($this->Dbo->begin()); + $this->assertTrue($model->delete(1)); + $this->assertEmpty($model->read(null, 1)); + $this->assertTrue($this->Dbo->rollback()); + $this->assertNotEmpty($model->read(null, 1)); + + $this->assertTrue($this->Dbo->begin()); + $this->assertTrue($model->delete(1)); + $this->assertEmpty($model->read(null, 1)); + $this->assertTrue($this->Dbo->commit()); + $this->assertEmpty($model->read(null, 1)); + + $this->assertTrue($this->Dbo->rollback()); + $this->assertNotEmpty($model->read(null, 1)); + } + } diff --git a/lib/Cake/Test/Case/Model/Datasource/Database/PostgresTest.php b/lib/Cake/Test/Case/Model/Datasource/Database/PostgresTest.php index 54b331f48..eb249e4d3 100644 --- a/lib/Cake/Test/Case/Model/Datasource/Database/PostgresTest.php +++ b/lib/Cake/Test/Case/Model/Datasource/Database/PostgresTest.php @@ -909,4 +909,37 @@ class PostgresTest extends CakeTestCase { $this->Dbo->truncate('articles'); } +/** + * Test nested transaction + * + * @return void + */ + public function testNestedTransaction() { + $this->skipIf($this->Dbo->supportNestedTransaction() === false, 'The Postgres server do not support nested transaction'); + + $this->loadFixtures('Article'); + $model = new Article(); + $model->hasOne = $model->hasMany = $model->belongsTo = $model->hasAndBelongsToMany = array(); + $model->cacheQueries = false; + $this->Dbo->cacheMethods = false; + + $this->assertTrue($this->Dbo->begin()); + $this->assertNotEmpty($model->read(null, 1)); + + $this->assertTrue($this->Dbo->begin()); + $this->assertTrue($model->delete(1)); + $this->assertEmpty($model->read(null, 1)); + $this->assertTrue($this->Dbo->rollback()); + $this->assertNotEmpty($model->read(null, 1)); + + $this->assertTrue($this->Dbo->begin()); + $this->assertTrue($model->delete(1)); + $this->assertEmpty($model->read(null, 1)); + $this->assertTrue($this->Dbo->commit()); + $this->assertEmpty($model->read(null, 1)); + + $this->assertTrue($this->Dbo->rollback()); + $this->assertNotEmpty($model->read(null, 1)); + } + } diff --git a/lib/Cake/Test/Case/Model/Datasource/Database/SqliteTest.php b/lib/Cake/Test/Case/Model/Datasource/Database/SqliteTest.php index f53257bff..a5a82cae1 100644 --- a/lib/Cake/Test/Case/Model/Datasource/Database/SqliteTest.php +++ b/lib/Cake/Test/Case/Model/Datasource/Database/SqliteTest.php @@ -383,4 +383,37 @@ class SqliteTest extends CakeTestCase { $this->assertTrue(Validation::uuid($result['Uuid']['id']), 'Not a uuid'); } +/** + * Test nested transaction + * + * @return void + */ + public function testNestedTransaction() { + $this->skipIf($this->Dbo->supportNestedTransaction() === false, 'The Sqlite version do not support nested transaction'); + + $this->loadFixtures('User'); + $model = new User(); + $model->hasOne = $model->hasMany = $model->belongsTo = $model->hasAndBelongsToMany = array(); + $model->cacheQueries = false; + $this->Dbo->cacheMethods = false; + + $this->assertTrue($this->Dbo->begin()); + $this->assertNotEmpty($model->read(null, 1)); + + $this->assertTrue($this->Dbo->begin()); + $this->assertTrue($model->delete(1)); + $this->assertEmpty($model->read(null, 1)); + $this->assertTrue($this->Dbo->rollback()); + $this->assertNotEmpty($model->read(null, 1)); + + $this->assertTrue($this->Dbo->begin()); + $this->assertTrue($model->delete(1)); + $this->assertEmpty($model->read(null, 1)); + $this->assertTrue($this->Dbo->commit()); + $this->assertEmpty($model->read(null, 1)); + + $this->assertTrue($this->Dbo->rollback()); + $this->assertNotEmpty($model->read(null, 1)); + } + } diff --git a/lib/Cake/Test/Case/Model/Datasource/DboSourceTest.php b/lib/Cake/Test/Case/Model/Datasource/DboSourceTest.php index bfd7483a6..a1f1c6f02 100644 --- a/lib/Cake/Test/Case/Model/Datasource/DboSourceTest.php +++ b/lib/Cake/Test/Case/Model/Datasource/DboSourceTest.php @@ -35,6 +35,8 @@ class MockDataSource extends DataSource { class DboTestSource extends DboSource { + public static $nested = true; + public function connect($config = array()) { $this->connected = true; } @@ -51,6 +53,10 @@ class DboTestSource extends DboSource { $this->_connection = $conn; } + public function supportNestedTransaction() { + return $this->nestedTransaction && self::$nested; + } + } /** @@ -834,6 +840,79 @@ class DboSourceTest extends CakeTestCase { $this->assertEquals($expected, $log['log'][0]); } +/** + * Test nested transaction calls + * + * @return void + */ + public function testTransactionNested() { + $conn = $this->getMock('MockPDO'); + $db = new DboTestSource(); + $db->setConnection($conn); + DboTestSource::$nested = true; + + $conn->expects($this->at(0))->method('beginTransaction')->will($this->returnValue(true)); + $conn->expects($this->at(1))->method('exec')->with($this->equalTo('SAVEPOINT LEVEL1'))->will($this->returnValue(true)); + $conn->expects($this->at(2))->method('exec')->with($this->equalTo('RELEASE SAVEPOINT LEVEL1'))->will($this->returnValue(true)); + $conn->expects($this->at(3))->method('exec')->with($this->equalTo('SAVEPOINT LEVEL1'))->will($this->returnValue(true)); + $conn->expects($this->at(4))->method('exec')->with($this->equalTo('ROLLBACK TO SAVEPOINT LEVEL1'))->will($this->returnValue(true)); + $conn->expects($this->at(5))->method('commit')->will($this->returnValue(true)); + + $this->_runTransactions($db); + } + +/** + * Test nested transaction calls without support + * + * @return void + */ + public function testTransactionNestedWithoutSupport() { + $conn = $this->getMock('MockPDO'); + $db = new DboTestSource(); + $db->setConnection($conn); + $db->nestedTransaction = false; + DboTestSource::$nested = true; + + $conn->expects($this->once())->method('beginTransaction')->will($this->returnValue(true)); + $conn->expects($this->never())->method('exec'); + $conn->expects($this->once())->method('commit')->will($this->returnValue(true)); + + $this->_runTransactions($db); + } + +/** + * Test nested transaction disabled + * + * @return void + */ + public function testTransactionNestedDisabled() { + $conn = $this->getMock('MockPDO'); + $db = new DboTestSource(); + $db->setConnection($conn); + DboTestSource::$nested = false; + + $conn->expects($this->once())->method('beginTransaction')->will($this->returnValue(true)); + $conn->expects($this->never())->method('exec'); + $conn->expects($this->once())->method('commit')->will($this->returnValue(true)); + + $this->_runTransactions($db); + } + +/** + * Nested transaction calls + * + * @param DboTestSource $db + * @return void + */ + protected function _runTransactions($db) { + $db->begin(); + $db->begin(); + $db->commit(); + $db->begin(); + $db->rollback(); + $db->commit(); + } + /** * Test build statement with some fields missing *