### Eclipse Workspace Patch 1.0 #P moodle20 Index: lib/dml/simpletest/testdml.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/dml/simpletest/testdml.php,v retrieving revision 1.56 diff -u -r1.56 testdml.php --- lib/dml/simpletest/testdml.php 16 Oct 2009 14:16:18 -0000 1.56 +++ lib/dml/simpletest/testdml.php 22 Oct 2009 15:25:13 -0000 @@ -2332,6 +2332,94 @@ $this->assertTrue(true, 'DB Transactions not supported. Test skipped'); } } + + function test_nested_transactions() { + $DB = $this->tdb; + $dbman = $DB->get_manager(); + + $table = $this->get_test_table(); + $tablename = $table->getName(); + + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0'); + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $dbman->create_table($table); + $this->tables[$tablename] = $table; + + $active = $DB->begin_sql(); + if ($active) { + // test only if driver supports transactions + + //two level rollback + $data = (object)array('course'=>3); + $DB->insert_record($tablename, $data); + $DB->begin_sql(); + $data = (object)array('course'=>4); + $DB->insert_record($tablename, $data); + $DB->commit_sql(); + $DB->rollback_sql(); + $this->assertEqual(0, $DB->count_records($tablename)); + + $DB->delete_records($tablename); + + // rollback from nested level + $DB->begin_sql(); + $data = (object)array('course'=>3); + $DB->insert_record($tablename, $data); + $DB->begin_sql(); + $data = (object)array('course'=>4); + $DB->insert_record($tablename, $data); + try { + $DB->rollback_sql(); + } catch (Exception $e) { + $this->assertTrue($e instanceof nested_rollback_exception); + } + // normally you would not commit the transaction when exception caught + // but for the purpose of testing we commit and make sure the data is + // not rolled back in nested exception + $DB->commit_sql(); + $this->assertEqual(2, $DB->count_records($tablename)); + + $DB->delete_records($tablename); + + $DB->begin_sql(); + $data = (object)array('course'=>3); + $DB->insert_record($tablename, $data); + $DB->begin_sql(); + $data = (object)array('course'=>4); + $DB->insert_record($tablename, $data); + $DB->commit_sql(); + $DB->commit_sql(); + $this->assertEqual(2, $DB->count_records($tablename)); + } else { + + $this->assertTrue(true, 'DB Transactions not supported. Test skipped'); + } + } + + function test_transaction_auto_rollback_on_error() { + $DB = $this->tdb; + + $DB->begin_sql(); + $DB->begin_sql(); + $DB->begin_sql(); + + //floowing code is copy/pasted from the default_exception_handler() + $i = 0; + while($DB->is_transaction_started()) { + $i++; + if ($i > 3) { + $this->fail('Force closing transaction failed'); + break; + } + //nested transactions throw exceptions, we need to traverse to the top level exception + try { + $DB->rollback_sql(); + } catch (Exception $ignored) { + } + } + $this->assertEqual(3, $i); + } } /** Index: lib/dml/pgsql_native_moodle_database.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/dml/pgsql_native_moodle_database.php,v retrieving revision 1.41 diff -u -r1.41 pgsql_native_moodle_database.php --- lib/dml/pgsql_native_moodle_database.php 16 Oct 2009 14:14:03 -0000 1.41 +++ lib/dml/pgsql_native_moodle_database.php 22 Oct 2009 15:25:11 -0000 @@ -1130,9 +1130,12 @@ * this is _very_ useful for massive updates */ public function begin_sql() { - if (!parent::begin_sql()) { - return false; + parent::begin_sql(); + if ($this->intransaction > 1) { + // do not start transactions in nested levels + return true; } + $sql = "BEGIN ISOLATION LEVEL READ COMMITTED"; $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = pg_query($this->pgsql, $sql); @@ -1146,9 +1149,12 @@ * on DBs that support it, commit the transaction */ public function commit_sql() { - if (!parent::commit_sql()) { - return false; + parent::commit_sql(); + if ($this->intransaction > 0) { + //nested transactions do not commit, only the top most one + return true; } + $sql = "COMMIT"; $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = pg_query($this->pgsql, $sql); @@ -1162,9 +1168,8 @@ * on DBs that support it, rollback the transaction */ public function rollback_sql() { - if (!parent::rollback_sql()) { - return false; - } + parent::rollback_sql(); + $sql = "ROLLBACK"; $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = pg_query($this->pgsql, $sql); Index: lib/dml/oci_native_moodle_database.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/dml/oci_native_moodle_database.php,v retrieving revision 1.29 diff -u -r1.29 oci_native_moodle_database.php --- lib/dml/oci_native_moodle_database.php 16 Oct 2009 14:14:03 -0000 1.29 +++ lib/dml/oci_native_moodle_database.php 22 Oct 2009 15:25:10 -0000 @@ -1528,9 +1528,12 @@ * this is _very_ useful for massive updates */ public function begin_sql() { - if (!parent::begin_sql()) { - return false; + parent::begin_sql(); + if ($this->intransaction > 1) { + // do not start transactions in nested levels + return true; } + $this->commit_status = OCI_DEFAULT; //Done! ;-) return true; } @@ -1539,8 +1542,10 @@ * on DBs that support it, commit the transaction */ public function commit_sql() { - if (!parent::commit_sql()) { - return false; + parent::commit_sql(); + if ($this->intransaction > 0) { + //nested transactions do not commit, only the top most one + return true; } $this->query_start('--oracle_commit', NULL, SQL_QUERY_AUX); @@ -1554,9 +1559,7 @@ * on DBs that support it, rollback the transaction */ public function rollback_sql() { - if (!parent::rollback_sql()) { - return false; - } + parent::rollback_sql(); $this->query_start('--oracle_rollback', NULL, SQL_QUERY_AUX); $result = oci_rollback($this->oci); Index: lib/dml/moodle_database.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/dml/moodle_database.php,v retrieving revision 1.104 diff -u -r1.104 moodle_database.php --- lib/dml/moodle_database.php 9 Oct 2009 10:02:39 -0000 1.104 +++ lib/dml/moodle_database.php 22 Oct 2009 15:25:07 -0000 @@ -109,8 +109,8 @@ /** @var bool true if db used for db sessions */ protected $used_for_db_sessions = false; - /** @var bool Flag indicating transaction in progress */ - protected $intransaction = false; + /** @var int Flag indicating transaction in progress and nesting level */ + protected $intransaction = 0; /** @var int internal temporary variable */ private $fix_sql_params_i; @@ -281,7 +281,7 @@ * Do NOT use connect() again, create a new instance if needed. */ public function dispose() { - if ($this->intransaction) { + if ($this->intransaction > 0) { // unfortunately we can not access global $CFG any more and can not print debug error_log('Active database transaction detected when disposing database!'); } @@ -1879,54 +1879,57 @@ * @return bool */ function is_transaction_started() { - return $this->intransaction; + return ($this->intransaction > 0); } /** - * on DBs that support it, switch to transaction mode and begin a transaction + * On DBs that support it, switch to transaction mode and begin a transaction * you'll need to ensure you call commit_sql() or your changes *will* be lost. + * Multiple levels of transactions are supported, the rollback always propagates + * to the top most level via exceptions. * * this is _very_ useful for massive updates * - * Please note only one level of transactions is supported, please do not use - * transaction in moodle core! Transaction are intended for web services - * enrolment and auth synchronisation scripts, etc. + * Please do not use transaction in moodle core yet! + * Transaction are intended for web services, external API functions, + * enrolments, auth synchronisation scripts, etc. * * @return bool success */ public function begin_sql() { - if ($this->intransaction) { - debugging('Transaction already in progress'); - return false; - } - $this->intransaction = true; - return true; + $this->intransaction++; + return false; } /** - * on DBs that support it, commit the transaction + * On DBs that support it, commit the transaction, + * throws exception if transaction not in progress. * @return bool success */ public function commit_sql() { - if (!$this->intransaction) { - debugging('Transaction not in progress'); - return false; + if ($this->intransaction < 1) { + throw new coding_exception('DB transaction not in progress, can not commit'); } - $this->intransaction = false; - return true; + $this->intransaction--; + return false; } /** - * on DBs that support it, rollback the transaction + * On DBs that support it, rollback the transaction, + * throws exception if requested in nested transaction + * because it needs to propagate to the top most level + * or the default_exception_handler(). * @return bool success */ public function rollback_sql() { - if (!$this->intransaction) { - debugging('Transaction not in progress'); - return false; + if ($this->intransaction < 1) { + throw new coding_exception('DB transaction not in progress, can not rollback'); } - $this->intransaction = false; - return true; + $this->intransaction--; + if ($this->intransaction > 0) { + throw new nested_rollback_exception(); + } + return false; } /// session locking Index: lib/dml/mssql_native_moodle_database.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/dml/mssql_native_moodle_database.php,v retrieving revision 1.12 diff -u -r1.12 mssql_native_moodle_database.php --- lib/dml/mssql_native_moodle_database.php 16 Oct 2009 14:14:03 -0000 1.12 +++ lib/dml/mssql_native_moodle_database.php 22 Oct 2009 15:25:08 -0000 @@ -1205,9 +1205,12 @@ * this is _very_ useful for massive updates */ public function begin_sql() { - if (!parent::begin_sql()) { - return false; + parent::begin_sql(); + if ($this->intransaction > 1) { + // do not start transactions in nested levels + return true; } + $sql = "BEGIN TRANSACTION"; // Will be using READ COMMITTED isolation $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = mssql_query($sql, $this->mssql); @@ -1222,9 +1225,12 @@ * on DBs that support it, commit the transaction */ public function commit_sql() { - if (!parent::commit_sql()) { - return false; + parent::commit_sql(); + if ($this->intransaction > 0) { + //nested transactions do not commit, only the top most one + return true; } + $sql = "COMMIT TRANSACTION"; $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = mssql_query($sql, $this->mssql); @@ -1239,9 +1245,8 @@ * on DBs that support it, rollback the transaction */ public function rollback_sql() { - if (!parent::rollback_sql()) { - return false; - } + parent::rollback_sql(); + $sql = "ROLLBACK TRANSACTION"; $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = mssql_query($sql, $this->mssql); Index: lib/dml/pdo_moodle_database.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/dml/pdo_moodle_database.php,v retrieving revision 1.21 diff -u -r1.21 pdo_moodle_database.php --- lib/dml/pdo_moodle_database.php 8 Oct 2009 22:34:35 -0000 1.21 +++ lib/dml/pdo_moodle_database.php 22 Oct 2009 15:25:11 -0000 @@ -543,8 +543,10 @@ } public function begin_sql() { - if (!parent::begin_sql()) { - return false; + parent::begin_sql(); + if ($this->intransaction > 1) { + // do not start transactions in nested levels + return true; } $this->query_start('', NULL, SQL_QUERY_AUX); @@ -559,9 +561,12 @@ $this->query_end($result); return $result; } + public function commit_sql() { - if (!parent::commit_sql()) { - return false; + parent::commit_sql(); + if ($this->intransaction > 0) { + //nested transactions do not commit, only the top most one + return true; } $this->query_start('', NULL, SQL_QUERY_AUX); @@ -578,9 +583,7 @@ } public function rollback_sql() { - if (!parent::rollback_sql()) { - return false; - } + parent::rollback_sql(); $this->query_start('', NULL, SQL_QUERY_AUX); $result = true; Index: lib/dml/mysqli_native_moodle_database.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/dml/mysqli_native_moodle_database.php,v retrieving revision 1.44 diff -u -r1.44 mysqli_native_moodle_database.php --- lib/dml/mysqli_native_moodle_database.php 8 Oct 2009 22:34:35 -0000 1.44 +++ lib/dml/mysqli_native_moodle_database.php 22 Oct 2009 15:25:09 -0000 @@ -1028,6 +1028,12 @@ * this is _very_ useful for massive updates */ public function begin_sql() { + parent::begin_sql(); + if ($this->intransaction > 1) { + // do not start transactions in nested levels + return true; + } + // Only will accept transactions if using InnoDB storage engine (more engines can be added easily BDB, Falcon...) $sql = "SELECT @@storage_engine"; $this->query_start($sql, NULL, SQL_QUERY_AUX); @@ -1042,10 +1048,6 @@ } $result->close(); - if (!parent::begin_sql()) { - return false; - } - $sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"; $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = $this->mysqli->query($sql); @@ -1063,9 +1065,12 @@ * on DBs that support it, commit the transaction */ public function commit_sql() { - if (!parent::commit_sql()) { - return false; + parent::commit_sql(); + if ($this->intransaction > 0) { + //nested transactions do not commit, only the top most one + return true; } + $sql = "COMMIT"; $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = $this->mysqli->query($sql); @@ -1078,9 +1083,8 @@ * on DBs that support it, rollback the transaction */ public function rollback_sql() { - if (!parent::rollback_sql()) { - return false; - } + parent::rollback_sql(); + $sql = "ROLLBACK"; $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = $this->mysqli->query($sql); Index: lib/setuplib.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/setuplib.php,v retrieving revision 1.78 diff -u -r1.78 setuplib.php --- lib/setuplib.php 15 Oct 2009 22:34:34 -0000 1.78 +++ lib/setuplib.php 22 Oct 2009 15:25:06 -0000 @@ -173,10 +173,13 @@ // detect active db transactions, rollback and log as error if ($DB && $DB->is_transaction_started()) { error_log('Database transaction aborted by exception in ' . $CFG->dirroot . $SCRIPT); - try { - // note: transaction blocks should never change current $_SESSION - $DB->rollback_sql(); - } catch (Exception $ignored) { + while($DB->is_transaction_started()) { + //nested transactions throw exceptions, we need to traverse to the top level exception + try { + // note: transaction blocks should never change current $_SESSION + $DB->rollback_sql(); + } catch (Exception $ignored) { + } } } Index: lib/dmllib.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/dmllib.php,v retrieving revision 1.183 diff -u -r1.183 dmllib.php --- lib/dmllib.php 8 Oct 2009 22:25:28 -0000 1.183 +++ lib/dmllib.php 22 Oct 2009 15:25:05 -0000 @@ -63,6 +63,17 @@ } /** + * Indicates nested transaction tried to rollback, + * we need to propage this request to the top most transaction + * instead or to the default_exception_handler(). + */ +class nested_rollback_exception extends dml_exception { + function __construct() { + parent::__construct('dmlnestedrollback'); + } +} + +/** * DML db connection exception - triggered if database not accessible. */ class dml_connection_exception extends dml_exception { Index: lang/en_utf8/error.php =================================================================== RCS file: /cvsroot/moodle/moodle/lang/en_utf8/error.php,v retrieving revision 1.207 diff -u -r1.207 error.php --- lang/en_utf8/error.php 5 Oct 2009 17:45:02 -0000 1.207 +++ lang/en_utf8/error.php 22 Oct 2009 15:25:05 -0000 @@ -178,6 +178,7 @@ $string['ddltablenotexist'] = 'Table \"$a\" does not exist'; $string['ddlunknownerror'] = 'Unknown DDL library error'; $string['ddlxmlfileerror'] = 'XML database file errors found'; +$string['dmlnestedrollback'] = 'Nested transaction rollback exception'; $string['dmlreadexception'] = 'Error reading from database'; $string['dmlwriteexception'] = 'Error writing to database'; $string['destinationcmnotexit'] = 'The destination course module does not exist';