### Eclipse Workspace Patch 1.0 #P moodle20 Index: mod/quiz/locallib.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/quiz/locallib.php,v retrieving revision 1.185 diff -u -r1.185 locallib.php --- mod/quiz/locallib.php 21 Oct 2009 04:23:36 -0000 1.185 +++ mod/quiz/locallib.php 4 Nov 2009 09:18:27 -0000 @@ -507,42 +507,44 @@ } // Use a transaction, so that on those databases that support it, this is safer. - $DB->begin_sql(); + $transaction = $DB->start_delegated_transaction(); - // Update the quiz table. - $success = $DB->set_field('quiz', 'grade', $newgrade, array('id' => $quiz->instance)); + try { + // Update the quiz table. + $DB->set_field('quiz', 'grade', $newgrade, array('id' => $quiz->instance)); + + // Rescaling the other data is only possible if the old grade was non-zero. + if ($quiz->grade > 1e-7) { + global $CFG; + + $factor = $newgrade/$quiz->grade; + $quiz->grade = $newgrade; + + // Update the quiz_grades table. + $timemodified = time(); + $DB->execute(" + UPDATE {quiz_grades} + SET grade = ? * grade, timemodified = ? + WHERE quiz = ? + ", array($factor, $timemodified, $quiz->id)); + + // Update the quiz_feedback table. + $DB->execute(" + UPDATE {quiz_feedback} + SET mingrade = ? * mingrade, maxgrade = ? * maxgrade + WHERE quizid = ? + ", array($factor, $factor, $quiz->id)); + } - // Rescaling the other data is only possible if the old grade was non-zero. - if ($quiz->grade > 1e-7) { - global $CFG; - - $factor = $newgrade/$quiz->grade; - $quiz->grade = $newgrade; - - // Update the quiz_grades table. - $timemodified = time(); - $success = $success && $DB->execute(" - UPDATE {quiz_grades} - SET grade = ? * grade, timemodified = ? - WHERE quiz = ? - ", array($factor, $timemodified, $quiz->id)); - - // Update the quiz_feedback table. - $success = $success && $DB->execute(" - UPDATE {quiz_feedback} - SET mingrade = ? * mingrade, maxgrade = ? * maxgrade - WHERE quizid = ? - ", array($factor, $factor, $quiz->id)); - } - - // update grade item and send all grades to gradebook - quiz_grade_item_update($quiz); - quiz_update_grades($quiz); + // update grade item and send all grades to gradebook + quiz_grade_item_update($quiz); + quiz_update_grades($quiz); - if ($success) { - return $DB->commit_sql(); - } else { - $DB->rollback_sql(); + $transaction->commit(); + return true; + + } catch (Exception $e) { + $transaction->rollback($e); return false; } } Index: lib/dml/simpletest/testdml.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/dml/simpletest/testdml.php,v retrieving revision 1.60 diff -u -r1.60 testdml.php --- lib/dml/simpletest/testdml.php 3 Nov 2009 19:53:11 -0000 1.60 +++ lib/dml/simpletest/testdml.php 4 Nov 2009 09:18:26 -0000 @@ -2297,7 +2297,12 @@ } - function test_begin_sql() { + /** + * Test some more complex SQL syntax which moodle uses and depends on to work + * useful to determine if new database libraries can be supported. + */ + public function test_get_records_sql_complicated() { + global $CFG; $DB = $this->tdb; $dbman = $DB->get_manager(); @@ -2310,19 +2315,25 @@ $dbman->create_table($table); $this->tables[$tablename] = $table; - $active = $DB->begin_sql(); - if ($active) { - // test only if driver supports transactions - $data = (object)array('course'=>3); - $DB->insert_record($tablename, $data); - $this->assertEqual(1, $DB->count_records($tablename)); - $DB->commit_sql(); - } else { - $this->assertTrue(true, 'DB Transactions not supported. Test skipped'); - } + $DB->insert_record($tablename, array('course' => 3)); + $DB->insert_record($tablename, array('course' => 3)); + $DB->insert_record($tablename, array('course' => 5)); + $DB->insert_record($tablename, array('course' => 2)); + + // we have sql like this in moodle, this syntax breaks on older versions of sqlite for example.. + $sql = 'SELECT a.id AS id, a.course AS course + FROM {'.$tablename.'} a + JOIN (SELECT * FROM {'.$tablename.'}) b + ON a.id = b.id + WHERE a.course = ?'; + + $this->assertTrue($records = $DB->get_records_sql($sql, array(3))); + $this->assertEqual(2, count($records)); + $this->assertEqual(1, reset($records)->id); + $this->assertEqual(2, next($records)->id); } - function test_commit_sql() { + function test_onelevel_commit() { $DB = $this->tdb; $dbman = $DB->get_manager(); @@ -2335,19 +2346,16 @@ $dbman->create_table($table); $this->tables[$tablename] = $table; - $active = $DB->begin_sql(); - if ($active) { - // test only if driver supports transactions - $data = (object)array('course'=>3); - $DB->insert_record($tablename, $data); - $DB->commit_sql(); - $this->assertEqual(1, $DB->count_records($tablename)); - } else { - $this->assertTrue(true, 'BD Transactions not supported. Test skipped'); - } + $transaction = $DB->start_delegated_transaction(); + $data = (object)array('course'=>3); + $this->assertEqual(0, $DB->count_records($tablename)); + $DB->insert_record($tablename, $data); + $this->assertEqual(1, $DB->count_records($tablename)); + $transaction->commit(); + $this->assertEqual(1, $DB->count_records($tablename)); } - function test_rollback_sql() { + function test_onelevel_rollback() { $DB = $this->tdb; $dbman = $DB->get_manager(); @@ -2360,24 +2368,22 @@ $dbman->create_table($table); $this->tables[$tablename] = $table; - $active = $DB->begin_sql(); - if ($active) { - // test only if driver supports transactions - $data = (object)array('course'=>3); - $DB->insert_record($tablename, $data); - $DB->rollback_sql(); - $this->assertEqual(0, $DB->count_records($tablename)); - } else { - $this->assertTrue(true, 'DB Transactions not supported. Test skipped'); + // this might in fact encourage ppl to migrate from myisam to innodb + + $transaction = $DB->start_delegated_transaction(); + $data = (object)array('course'=>3); + $this->assertEqual(0, $DB->count_records($tablename)); + $DB->insert_record($tablename, $data); + $this->assertEqual(1, $DB->count_records($tablename)); + try { + $transaction->rollback(new Exception('test')); + $this->fail('transaction rollback must rethrow exception'); + } catch (Exception $e) { } + $this->assertEqual(0, $DB->count_records($tablename)); } - /** - * Test some more complex SQL syntax which moodle uses and depends on to work - * useful to determine if new database libraries can be supported. - */ - public function test_get_records_sql_complicated() { - global $CFG; + function test_nested_transactions() { $DB = $this->tdb; $dbman = $DB->get_manager(); @@ -2390,22 +2396,161 @@ $dbman->create_table($table); $this->tables[$tablename] = $table; - $DB->insert_record($tablename, array('course' => 3)); - $DB->insert_record($tablename, array('course' => 3)); - $DB->insert_record($tablename, array('course' => 5)); - $DB->insert_record($tablename, array('course' => 2)); + // two level commit + $this->assertFalse($DB->is_transaction_started()); + $transaction1 = $DB->start_delegated_transaction(); + $this->assertTrue($DB->is_transaction_started()); + $data = (object)array('course'=>3); + $DB->insert_record($tablename, $data); + $transaction2 = $DB->start_delegated_transaction(); + $data = (object)array('course'=>4); + $DB->insert_record($tablename, $data); + $transaction2->commit(); + $this->assertTrue($DB->is_transaction_started()); + $transaction1->commit(); + $this->assertFalse($DB->is_transaction_started()); + $this->assertEqual(2, $DB->count_records($tablename)); + + $DB->delete_records($tablename); + + // rollback from top level + $transaction1 = $DB->start_delegated_transaction(); + $data = (object)array('course'=>3); + $DB->insert_record($tablename, $data); + $transaction2 = $DB->start_delegated_transaction(); + $data = (object)array('course'=>4); + $DB->insert_record($tablename, $data); + $transaction2->commit(); + try { + $transaction1->rollback(new Exception('test')); + $this->fail('transaction rollback must rethrow exception'); + } catch (Exception $e) { + $this->assertEqual(get_class($e), 'Exception'); + } + $this->assertEqual(0, $DB->count_records($tablename)); - // we have sql like this in moodle, this syntax breaks on older versions of sqlite for example.. - $sql = 'SELECT a.id AS id, a.course AS course - FROM {'.$tablename.'} a - JOIN (SELECT * FROM {'.$tablename.'}) b - ON a.id = b.id - WHERE a.course = ?'; + $DB->delete_records($tablename); - $this->assertTrue($records = $DB->get_records_sql($sql, array(3))); - $this->assertEqual(2, count($records)); - $this->assertEqual(1, reset($records)->id); - $this->assertEqual(2, next($records)->id); + // rollback from nested level + $transaction1 = $DB->start_delegated_transaction(); + $data = (object)array('course'=>3); + $DB->insert_record($tablename, $data); + $transaction2 = $DB->start_delegated_transaction(); + $data = (object)array('course'=>4); + $DB->insert_record($tablename, $data); + try { + $transaction2->rollback(new Exception('test')); + $this->fail('transaction rollback must rethrow exception'); + } catch (Exception $e) { + $this->assertEqual(get_class($e), 'Exception'); + } + $this->assertEqual(2, $DB->count_records($tablename)); // not rolled back yet + try { + $transaction1->commit(); + } catch (Exception $e) { + $this->assertEqual(get_class($e), 'dml_transaction_exception'); + } + $this->assertEqual(2, $DB->count_records($tablename)); // not rolled back yet + // the forced rollback is done from the default_exception hadnler and similar places, + // let's do it manually here + $this->assertTrue($DB->is_transaction_started()); + $DB->force_transaction_rollback(); + $this->assertFalse($DB->is_transaction_started()); + $this->assertEqual(0, $DB->count_records($tablename)); // finally rolled back + + $DB->delete_records($tablename); + } + + function test_wrong_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; + + + // wrong order of nested commits + $transaction1 = $DB->start_delegated_transaction(); + $data = (object)array('course'=>3); + $DB->insert_record($tablename, $data); + $transaction2 = $DB->start_delegated_transaction(); + $data = (object)array('course'=>4); + $DB->insert_record($tablename, $data); + try { + $transaction1->commit(); + $this->fail('wrong order of commits must throw exception'); + } catch (Exception $e) { + $this->assertEqual(get_class($e), 'dml_transaction_exception'); + } + try { + $transaction2->commit(); + $this->fail('first wrong commit forces rollback'); + } catch (Exception $e) { + $this->assertEqual(get_class($e), 'dml_transaction_exception'); + } + // this is done in default exception hadnler usually + $this->assertTrue($DB->is_transaction_started()); + $this->assertEqual(2, $DB->count_records($tablename)); // not rolled back yet + $DB->force_transaction_rollback(); + $this->assertEqual(0, $DB->count_records($tablename)); + $DB->delete_records($tablename); + + + // wrong order of nested rollbacks + $transaction1 = $DB->start_delegated_transaction(); + $data = (object)array('course'=>3); + $DB->insert_record($tablename, $data); + $transaction2 = $DB->start_delegated_transaction(); + $data = (object)array('course'=>4); + $DB->insert_record($tablename, $data); + try { + // this first rollback should prevent all otehr rollbacks + $transaction1->rollback(new Exception('test')); + } catch (Exception $e) { + $this->assertEqual(get_class($e), 'Exception'); + } + try { + $transaction2->rollback(new Exception('test')); + } catch (Exception $e) { + $this->assertEqual(get_class($e), 'Exception'); + } + try { + $transaction1->rollback(new Exception('test')); + } catch (Exception $e) { + // the rollback was used already once, no way to use it again + $this->assertEqual(get_class($e), 'dml_transaction_exception'); + } + // this is done in default exception hadnler usually + $this->assertTrue($DB->is_transaction_started()); + $DB->force_transaction_rollback(); + $DB->delete_records($tablename); + + + // unknown transaction object + $transaction1 = $DB->start_delegated_transaction(); + $data = (object)array('course'=>3); + $DB->insert_record($tablename, $data); + $transaction2 = new moodle_transaction($DB); + try { + $transaction2->commit(); + $this->fail('foreign transaction must fail'); + } catch (Exception $e) { + $this->assertEqual(get_class($e), 'dml_transaction_exception'); + } + try { + $transaction1->commit(); + $this->fail('first wrong commit forces rollback'); + } catch (Exception $e) { + $this->assertEqual(get_class($e), 'dml_transaction_exception'); + } + $DB->force_transaction_rollback(); + $DB->delete_records($tablename); } } @@ -2453,4 +2598,7 @@ public function sql_concat(){} public function sql_concat_join($separator="' '", $elements=array()){} public function sql_substr($expr, $start, $length=false){} + public function begin_transaction() {} + public function commit_transaction() {} + public function rollback_transaction() {} } Index: lib/setuplib.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/setuplib.php,v retrieving revision 1.89 diff -u -r1.89 setuplib.php --- lib/setuplib.php 4 Nov 2009 08:49:22 -0000 1.89 +++ lib/setuplib.php 4 Nov 2009 09:18:20 -0000 @@ -208,14 +208,12 @@ function abort_all_db_transactions() { global $CFG, $DB, $SCRIPT; + // default exception handler MUST not throw any exceptions!! + if ($DB && $DB->is_transaction_started()) { error_log('Database transaction aborted automatically in ' . $CFG->dirroot . $SCRIPT); - try { - // note: transaction blocks should never change current $_SESSION - $DB->rollback_sql(); - } catch (Exception $ignored) { - // default exception handler MUST not throw any exceptions!! - } + // note: transaction blocks should never change current $_SESSION + $DB->force_transaction_rollback(); } } Index: lib/outputrenderers.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/outputrenderers.php,v retrieving revision 1.41 diff -u -r1.41 outputrenderers.php --- lib/outputrenderers.php 4 Nov 2009 03:22:26 -0000 1.41 +++ lib/outputrenderers.php 4 Nov 2009 09:18:17 -0000 @@ -874,12 +874,16 @@ * @return string HTML fragment */ public function footer() { - global $CFG; + global $CFG, $DB; $output = $this->opencontainers->pop_all_but_last(true); $footer = $this->opencontainers->pop('header/footer'); + if (debugging() and $DB and $DB->is_transaction_started()) { + // TODO: MDL-20625 print warning - transaction will be rolled back + } + // Provide some performance info if required $performanceinfo = ''; if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) { Index: lib/dmllib.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/dmllib.php,v retrieving revision 1.184 diff -u -r1.184 dmllib.php --- lib/dmllib.php 1 Nov 2009 11:31:19 -0000 1.184 +++ lib/dmllib.php 4 Nov 2009 09:18:09 -0000 @@ -197,6 +197,23 @@ } /** + * DML transaction exception - triggered by probles related to DB transactions + */ +class dml_transaction_exception extends dml_exception { + /** @var moodle_transaction */ + public $transaction; + + /** + * Constructor + * @param array $start_backtrace + */ + function __construct($debuginfo=null, $transaction=null) { + $this->transaction = $transaction; // TODO: MDL-20625 use the info from $transaction for debugging purposes + parent::__construct('dmltransactionexception', NULL, $debuginfo); + } +} + +/** * Sets up global $DB moodle_database instance * * @global object Index: lib/accesslib.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/accesslib.php,v retrieving revision 1.622 diff -u -r1.622 accesslib.php --- lib/accesslib.php 4 Nov 2009 06:11:32 -0000 1.622 +++ lib/accesslib.php 4 Nov 2009 09:18:09 -0000 @@ -2455,24 +2455,18 @@ ON c.instanceid = t.id WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK." "; + + // transactions used only for performance reasons here + $transaction = $DB->start_delegated_transaction(); + if ($rs = $DB->get_recordset_sql($sql)) { - $DB->begin_sql(); - $ok = true; foreach ($rs as $ctx) { - if (!delete_context($ctx->contextlevel, $ctx->instanceid)) { - $ok = false; - break; - } + delete_context($ctx->contextlevel, $ctx->instanceid); } $rs->close(); - if ($ok) { - $DB->commit_sql(); - return true; - } else { - $DB->rollback_sql(); - return false; - } } + + $transaction->commit(); return true; } Index: lib/moodlelib.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/moodlelib.php,v retrieving revision 1.1260 diff -u -r1.1260 moodlelib.php --- lib/moodlelib.php 3 Nov 2009 21:02:36 -0000 1.1260 +++ lib/moodlelib.php 4 Nov 2009 09:18:15 -0000 @@ -3392,61 +3392,50 @@ require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/message/lib.php'); - // TODO: decide if this transaction is really needed - $DB->begin_sql(); - - try { - // delete all grades - backup is kept in grade_grades_history table - if ($grades = grade_grade::fetch_all(array('userid'=>$user->id))) { - foreach ($grades as $grade) { - $grade->delete('userdelete'); - } + // delete all grades - backup is kept in grade_grades_history table + if ($grades = grade_grade::fetch_all(array('userid'=>$user->id))) { + foreach ($grades as $grade) { + $grade->delete('userdelete'); } + } - //move unread messages from this user to read - message_move_userfrom_unread2read($user->id); - - // remove from all groups - $DB->delete_records('groups_members', array('userid'=>$user->id)); + //move unread messages from this user to read + message_move_userfrom_unread2read($user->id); - // unenrol from all roles in all contexts - role_unassign(0, $user->id); // this might be slow but it is really needed - modules might do some extra cleanup! + // remove from all groups + $DB->delete_records('groups_members', array('userid'=>$user->id)); - // now do a final accesslib cleanup - removes all role assingments in user context and context itself - delete_context(CONTEXT_USER, $user->id); + // unenrol from all roles in all contexts + role_unassign(0, $user->id); // this might be slow but it is really needed - modules might do some extra cleanup! - require_once($CFG->dirroot.'/tag/lib.php'); - tag_set('user', $user->id, array()); + // now do a final accesslib cleanup - removes all role assingments in user context and context itself + delete_context(CONTEXT_USER, $user->id); - // workaround for bulk deletes of users with the same email address - $delname = "$user->email.".time(); - while ($DB->record_exists('user', array('username'=>$delname))) { // no need to use mnethostid here - $delname++; - } + require_once($CFG->dirroot.'/tag/lib.php'); + tag_set('user', $user->id, array()); - // mark internal user record as "deleted" - $updateuser = new object(); - $updateuser->id = $user->id; - $updateuser->deleted = 1; - $updateuser->username = $delname; // Remember it just in case - $updateuser->email = ''; // Clear this field to free it up - $updateuser->idnumber = ''; // Clear this field to free it up - $updateuser->timemodified = time(); - - $DB->update_record('user', $updateuser); + // workaround for bulk deletes of users with the same email address + $delname = "$user->email.".time(); + while ($DB->record_exists('user', array('username'=>$delname))) { // no need to use mnethostid here + $delname++; + } - $DB->commit_sql(); + // mark internal user record as "deleted" + $updateuser = new object(); + $updateuser->id = $user->id; + $updateuser->deleted = 1; + $updateuser->username = $delname; // Remember it just in case + $updateuser->email = ''; // Clear this field to free it up + $updateuser->idnumber = ''; // Clear this field to free it up + $updateuser->timemodified = time(); - // notify auth plugin - do not block the delete even when plugin fails - $authplugin = get_auth_plugin($user->auth); - $authplugin->user_delete($user); + $DB->update_record('user', $updateuser); - events_trigger('user_deleted', $user); + // notify auth plugin - do not block the delete even when plugin fails + $authplugin = get_auth_plugin($user->auth); + $authplugin->user_delete($user); - } catch (Exception $e) { - $DB->rollback_sql(); - throw $e; - } + events_trigger('user_deleted', $user); return true; } Index: lib/outputlib.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/outputlib.php,v retrieving revision 1.90 diff -u -r1.90 outputlib.php --- lib/outputlib.php 30 Sep 2009 16:24:05 -0000 1.90 +++ lib/outputlib.php 4 Nov 2009 09:18:15 -0000 @@ -859,6 +859,9 @@ return; } + // TODO: MDL-20625 this looks dangerous and problematic because we never know + // the order of calling of constructors ==> the transaction warning will not be included + // It seems you cannot rely on $CFG, and hence the debugging function here, // becuase $CFG may be destroyed before this object is. if ($this->isdebugging) { Index: enrol/database/enrol.php =================================================================== RCS file: /cvsroot/moodle/moodle/enrol/database/enrol.php,v retrieving revision 1.57 diff -u -r1.57 enrol.php --- enrol/database/enrol.php 1 Nov 2009 12:58:51 -0000 1.57 +++ enrol/database/enrol.php 4 Nov 2009 09:18:04 -0000 @@ -219,7 +219,8 @@ return true; } - $DB->begin_sql(); + $transaction = $DB->start_delegated_transaction(); + $extcourses = array(); while ($extcourse_obj = (object)$rs->FetchRow()) { // there are more course records $extcourse_obj = (object)array_change_key_case((array)$extcourse_obj , CASE_LOWER); @@ -416,7 +417,7 @@ $ers->close(); // release the handle } - $DB->commit_sql(); + $transaction->commit(); // we are done now, a bit of housekeeping fix_course_sortorder(); Index: lib/dtl/database_importer.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/dtl/database_importer.php,v retrieving revision 1.5 diff -u -r1.5 database_importer.php --- lib/dtl/database_importer.php 12 Jun 2009 08:50:50 -0000 1.5 +++ lib/dtl/database_importer.php 4 Nov 2009 09:18:26 -0000 @@ -57,6 +57,8 @@ * How to use transactions. */ protected $transactionmode = 'allinone'; + /** Transaction object */ + protected $transaction; /** * Object constructor. @@ -123,7 +125,7 @@ throw new dbtransfer_exception('importschemaexception', $details); } if ($this->transactionmode == 'allinone') { - $this->mdb->begin_sql(); + $this->transaction = $this->mdb->start_delegated_transaction(); } } @@ -140,7 +142,7 @@ */ public function begin_table_import($tablename, $schemaHash) { if ($this->transactionmode == 'pertable') { - $this->mdb->begin_sql(); + $this->transaction = $this->mdb->start_delegated_transaction(); } if (!$table = $this->schema->getTable($tablename)) { throw new dbtransfer_exception('unknowntableexception', $tablename); @@ -171,7 +173,7 @@ } } if ($this->transactionmode == 'pertable') { - $this->mdb->commit_sql(); + $this->transaction->commit(); } } @@ -182,7 +184,7 @@ */ public function finish_database_import() { if ($this->transactionmode == 'allinone') { - $this->mdb->commit_sql(); + $this->transaction->commit(); } } Index: group/delete.php =================================================================== RCS file: /cvsroot/moodle/moodle/group/delete.php,v retrieving revision 1.8 diff -u -r1.8 delete.php --- group/delete.php 1 Nov 2009 12:07:25 -0000 1.8 +++ group/delete.php 4 Nov 2009 09:18:04 -0000 @@ -49,11 +49,11 @@ if (!confirm_sesskey() ) { print_error('confirmsesskeybad','error',$returnurl); } - $DB->begin_sql(); + foreach($groupidarray as $groupid) { groups_delete_group($groupid); } - $DB->commit_sql(); + redirect($returnurl); } else { $PAGE->set_title(get_string('deleteselectedgroup', 'group')); Index: group/externallib.php =================================================================== RCS file: /cvsroot/moodle/moodle/group/externallib.php,v retrieving revision 1.17 diff -u -r1.17 externallib.php --- group/externallib.php 1 Nov 2009 12:07:25 -0000 1.17 +++ group/externallib.php 4 Nov 2009 09:18:04 -0000 @@ -60,36 +60,31 @@ $params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups)); - // ideally create all groups or none at all, unfortunately myisam engine does not support transactions :-( - $DB->begin_sql(); - try { -//TODO: there is a potential problem with events propagating actions to external systems :-( - $groups = array(); - - foreach ($params['groups'] as $group) { - $group = (object)$group; - - if (trim($group->name) == '') { - throw new invalid_parameter_exception('Invalid group name'); - } - if ($DB->get_record('groups', array('courseid'=>$group->courseid, 'name'=>$group->name))) { - throw new invalid_parameter_exception('Group with the same name already exists in the course'); - } - - // now security checks - $context = get_context_instance(CONTEXT_COURSE, $group->courseid); - self::validate_context($context); - require_capability('moodle/course:managegroups', $context); - - // finally create the group - $group->id = groups_create_group($group, false); - $groups[] = (array)$group; + $transaction = $DB->start_delegated_transaction(); + + $groups = array(); + + foreach ($params['groups'] as $group) { + $group = (object)$group; + + if (trim($group->name) == '') { + throw new invalid_parameter_exception('Invalid group name'); + } + if ($DB->get_record('groups', array('courseid'=>$group->courseid, 'name'=>$group->name))) { + throw new invalid_parameter_exception('Group with the same name already exists in the course'); } - } catch (Exception $ex) { - $DB->rollback_sql(); - throw $ex; + + // now security checks + $context = get_context_instance(CONTEXT_COURSE, $group->courseid); + self::validate_context($context); + require_capability('moodle/course:managegroups', $context); + + // finally create the group + $group->id = groups_create_group($group, false); + $groups[] = (array)$group; } - $DB->commit_sql(); + + $transaction->commit(); return $groups; } @@ -242,30 +237,27 @@ $params = self::validate_parameters(self::delete_groups_parameters(), array('groupids'=>$groupids)); - $DB->begin_sql(); - try { + $transaction = $DB->start_delegated_transaction(); + // TODO: this is problematic because the DB rollback does not handle deleting of images!! // there is also potential problem with events propagating action to external systems :-( - foreach ($params['groupids'] as $groupid) { - // validate params - $groupid = validate_param($groupid, PARAM_INTEGER); - if (!$group = groups_get_group($groupid, 'id, courseid', IGNORE_MISSING)) { - // silently ignore attempts to delete nonexisting groups - continue; - } - - // now security checks - $context = get_context_instance(CONTEXT_COURSE, $group->courseid); - self::validate_context($context); - require_capability('moodle/course:managegroups', $context); - - groups_delete_group($group); + foreach ($params['groupids'] as $groupid) { + // validate params + $groupid = validate_param($groupid, PARAM_INTEGER); + if (!$group = groups_get_group($groupid, 'id, courseid', IGNORE_MISSING)) { + // silently ignore attempts to delete nonexisting groups + continue; } - } catch (Exception $ex) { - $DB->rollback_sql(); - throw $ex; + + // now security checks + $context = get_context_instance(CONTEXT_COURSE, $group->courseid); + self::validate_context($context); + require_capability('moodle/course:managegroups', $context); + + groups_delete_group($group); } - $DB->commit_sql(); + + $transaction->commit(); } /** @@ -357,29 +349,26 @@ $params = self::validate_parameters(self::add_groupmembers_parameters(), array('members'=>$members)); - $DB->begin_sql(); - try { + $transaction = $DB->start_delegated_transaction(); + // TODO: there is a potential problem with events propagating action to external systems :-( - foreach ($params['members'] as $member) { - // validate params - $groupid = $member['groupid']; - $userid = $member['userid']; - - $group = groups_get_group($groupid, 'id, courseid', MUST_EXIST); - $user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST); - - // now security checks - $context = get_context_instance(CONTEXT_COURSE, $group->courseid); - self::validate_context($context); - require_capability('moodle/course:managegroups', $context); + foreach ($params['members'] as $member) { + // validate params + $groupid = $member['groupid']; + $userid = $member['userid']; - groups_add_member($group, $user); - } - } catch (Exception $ex) { - $DB->rollback_sql(); - throw $ex; + $group = groups_get_group($groupid, 'id, courseid', MUST_EXIST); + $user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST); + + // now security checks + $context = get_context_instance(CONTEXT_COURSE, $group->courseid); + self::validate_context($context); + require_capability('moodle/course:managegroups', $context); + + groups_add_member($group, $user); } - $DB->commit_sql(); + + $transaction->commit(); } /** @@ -417,29 +406,26 @@ $params = self::validate_parameters(self::delete_groupmembers_parameters(), array($members=>'members')); - $DB->begin_sql(); - try { + $transaction = $DB->start_delegated_transaction(); + // TODO: there is a potential problem with events propagating action to external systems :-( foreach ($params['members'] as $member) { - // validate params - $groupid = $member['groupid']; - $userid = $member['userid']; - - $group = groups_get_group($groupid, 'id, courseid', MUST_EXIST); - $user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST); - - // now security checks - $context = get_context_instance(CONTEXT_COURSE, $group->courseid); - self::validate_context($context); - require_capability('moodle/course:managegroups', $context); + // validate params + $groupid = $member['groupid']; + $userid = $member['userid']; - groups_remove_member($group, $user); - } - } catch (Exception $ex) { - $DB->rollback_sql(); - throw $ex; + $group = groups_get_group($groupid, 'id, courseid', MUST_EXIST); + $user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST); + + // now security checks + $context = get_context_instance(CONTEXT_COURSE, $group->courseid); + self::validate_context($context); + require_capability('moodle/course:managegroups', $context); + + groups_remove_member($group, $user); } - $DB->commit_sql(); + + $transaction->commit(); } /** 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 4 Nov 2009 09:18:20 -0000 @@ -27,6 +27,7 @@ require_once($CFG->libdir.'/dml/database_column_info.php'); require_once($CFG->libdir.'/dml/moodle_recordset.php'); +require_once($CFG->libdir.'/dml/moodle_transaction.php'); /// GLOBAL CONSTANTS ///////////////////////////////////////////////////////// @@ -109,8 +110,10 @@ /** @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 array open transactions */ + private $transactions = array(); + /** @var bool force rollback of all current transactions */ + private $force_rollback = false; /** @var int internal temporary variable */ private $fix_sql_params_i; @@ -281,8 +284,10 @@ * Do NOT use connect() again, create a new instance if needed. */ public function dispose() { - if ($this->intransaction) { - // unfortunately we can not access global $CFG any more and can not print debug + if ($this->transactions) { + // unfortunately we can not access global $CFG any more and can not print debug, + // the diagnostic info should be printed in footer instead + $this->force_transaction_rollback(); error_log('Active database transaction detected when disposing database!'); } if ($this->used_for_db_sessions) { @@ -1879,54 +1884,150 @@ * @return bool */ function is_transaction_started() { - return $this->intransaction; + return !empty($this->transactions); } /** - * 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. + * On DBs that support it, switch to transaction mode and begin a transaction + * you'll need to ensure you call commit() or your changes *will* be lost. * * 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. + * Delegated database transactions can be nested, but only one actual database + * transaction is used for the outer-most delegated transaction. This method + * returns a transaction object which you should keep until the end of the + * delegated transaction. The actual database transaction will + * only be committed if all the nested delegated transactions commit + * successfully. If any part of the transaction rolls back then the whole + * thing is rolled back. * - * @return bool success + * @return moodle_transaction */ - public function begin_sql() { - if ($this->intransaction) { - debugging('Transaction already in progress'); - return false; + public function start_delegated_transaction() { + $transaction = new moodle_transaction($this); + $this->transactions[] = $transaction; + if (count($this->transactions) == 1) { + $this->begin_transaction(); } - $this->intransaction = true; - return true; + return $transaction; } /** - * on DBs that support it, commit the transaction - * @return bool success + * Driver specific start of real database transaction, + * this can not be used directly in code. + * @return void */ - public function commit_sql() { - if (!$this->intransaction) { - debugging('Transaction not in progress'); - return false; + protected abstract function begin_transaction(); + + /** + * Indicates delegated transaction finished successfully. + * The real database transaction is committed only if + * all delegated transactions committed. + * @return void + */ + public function commit_delegated_transaction(moodle_transaction $transaction) { + if ($transaction->is_disposed()) { + throw new dml_transaction_exception('Transactions already disposed', $transaction); } - $this->intransaction = false; - return true; + // mark as disposed so that it can not be used again + $transaction->dispose(); + + if (empty($this->transactions)) { + throw new dml_transaction_exception('Transaction not started', $transaction); + } + + if ($this->force_rollback) { + throw new dml_transaction_exception('Tried to commit transaction after lower level rollback', $transaction); + } + + if ($transaction !== $this->transactions[count($this->transactions) - 1]) { + // one incorrect commit at any level rollbacks everything + $this->force_rollback = true; + throw new dml_transaction_exception('Invalid transaction commit attempt', $transaction); + } + + if (count($this->transactions) == 1) { + // only commit the top most level + $this->commit_transaction(); + } + array_pop($this->transactions); } /** - * on DBs that support it, rollback the transaction - * @return bool success + * Driver specific commit of real database transaction, + * this can not be used directly in code. + * @return void */ - public function rollback_sql() { - if (!$this->intransaction) { - debugging('Transaction not in progress'); - return false; + protected abstract function commit_transaction(); + + /** + * Call when delegated transaction failed, this rolls back + * all delegated transactions up to the top most level. + * + * In many cases you do not need to call this method manually, + * because all open delegated transactions are rolled back + * automatically if exceptions not caucht. + * + * @param moodle_transaction $transaction + * @param Exception $e exception that caused the problem + * @return does not return, exception is rethrown + */ + public function rollback_delegated_transaction(moodle_transaction $transaction, Exception $e) { + if ($transaction->is_disposed()) { + throw new dml_transaction_exception('Transactions already disposed', $transaction); } - $this->intransaction = false; - return true; + // mark as disposed so that it can not be used again + $transaction->dispose(); + + // one rollback at any level rollbacks everything + $this->force_rollback = true; + + if (empty($this->transactions) or $transaction !== $this->transactions[count($this->transactions) - 1]) { + // this may or may not be a coding problem, better just rethrow the exception, + // because we do not want to loose the original $e + throw $e; + } + + if (count($this->transactions) == 1) { + // only rollback the top most level + $this->rollback_transaction(); + } + array_pop($this->transactions); + if (empty($this->transactions)) { + // finally top most level rolled back + $this->force_rollback = false; + } + throw $e; + } + + /** + * Driver specific bort of real database transaction, + * this can not be used directly in code. + * @return void + */ + protected abstract function rollback_transaction(); + + /** + * Force rollback of all delegted transaction. + * Does not trow any exceptions and does not log anything. + * + * This method should be used only from default exception handlers and other + * core code. + * + * @return void + */ + public function force_transaction_rollback() { + if ($this->transactions) { + try { + $this->rollback_transaction(); + } catch (dml_exception $e) { + // ignore any sql errors here, the connection might be broken + } + } + + // now enable transactions again + $this->transactions = array(); // unfortunately all unfinished exceptions are kept in memory + $this->force_rollback = 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 4 Nov 2009 09:18:20 -0000 @@ -1199,56 +1199,44 @@ /// transactions /** - * 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. - * - * this is _very_ useful for massive updates + * Driver specific start of real database transaction, + * this can not be used directly in code. + * @return void */ - public function begin_sql() { - if (!parent::begin_sql()) { - return false; - } + protected function begin_transaction() { $sql = "BEGIN TRANSACTION"; // Will be using READ COMMITTED isolation $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = mssql_query($sql, $this->mssql); $this->query_end($result); $this->free_result($result); - - return true; } /** - * on DBs that support it, commit the transaction + * Driver specific commit of real database transaction, + * this can not be used directly in code. + * @return void */ - public function commit_sql() { - if (!parent::commit_sql()) { - return false; - } + protected function commit_transaction() { $sql = "COMMIT TRANSACTION"; $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = mssql_query($sql, $this->mssql); $this->query_end($result); $this->free_result($result); - - return true; } /** - * on DBs that support it, rollback the transaction + * Driver specific abort of real database transaction, + * this can not be used directly in code. + * @return void */ - public function rollback_sql() { - if (!parent::rollback_sql()) { - return false; - } + protected function rollback_transaction() { $sql = "ROLLBACK TRANSACTION"; $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = mssql_query($sql, $this->mssql); $this->query_end($result); $this->free_result($result); - - return true; } } 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 4 Nov 2009 09:18:23 -0000 @@ -542,57 +542,36 @@ print_error('TODO'); } - public function begin_sql() { - if (!parent::begin_sql()) { - return false; - } - + protected function begin_transaction() { $this->query_start('', NULL, SQL_QUERY_AUX); - $result = true; - try { $this->pdb->beginTransaction(); } catch(PDOException $ex) { $this->lastError = $ex->getMessage(); - $result = false; } $this->query_end($result); - return $result; } - public function commit_sql() { - if (!parent::commit_sql()) { - return false; - } + protected function commit_transaction() { $this->query_start('', NULL, SQL_QUERY_AUX); - $result = true; try { $this->pdb->commit(); } catch(PDOException $ex) { $this->lastError = $ex->getMessage(); - $result = false; } $this->query_end($result); - return $result; } - public function rollback_sql() { - if (!parent::rollback_sql()) { - return false; - } - + protected function rollback_transaction() { $this->query_start('', NULL, SQL_QUERY_AUX); - $result = true; try { $this->pdb->rollBack(); } catch(PDOException $ex) { $this->lastError = $ex->getMessage(); - $result = false; } $this->query_end($result); - return $result; } /** Index: lib/dml/mysqli_native_moodle_database.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/dml/mysqli_native_moodle_database.php,v retrieving revision 1.45 diff -u -r1.45 mysqli_native_moodle_database.php --- lib/dml/mysqli_native_moodle_database.php 3 Nov 2009 23:34:43 -0000 1.45 +++ lib/dml/mysqli_native_moodle_database.php 4 Nov 2009 09:18:22 -0000 @@ -1039,12 +1039,11 @@ /// transactions /** - * 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. - * - * this is _very_ useful for massive updates + * Driver specific start of real database transaction, + * this can not be used directly in code. + * @return void */ - public function begin_sql() { + protected function begin_transaction() { // 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); @@ -1052,17 +1051,13 @@ $this->query_end($result); if ($rec = $result->fetch_assoc()) { if (!in_array($rec['@@storage_engine'], array('InnoDB'))) { - return false; + return; } } else { - return false; + return; } $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); @@ -1072,29 +1067,26 @@ $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = $this->mysqli->query($sql); $this->query_end($result); - - return true; } /** - * on DBs that support it, commit the transaction + * Driver specific commit of real database transaction, + * this can not be used directly in code. + * @return void */ - public function commit_sql() { - if (!parent::commit_sql()) { - return false; - } + protected function commit_transaction() { $sql = "COMMIT"; $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = $this->mysqli->query($sql); $this->query_end($result); - - return true; } /** - * on DBs that support it, rollback the transaction + * Driver specific abort of real database transaction, + * this can not be used directly in code. + * @return void */ - public function rollback_sql() { + protected function rollback_transaction() { if (!parent::rollback_sql()) { return false; } Index: lib/dml/pgsql_native_moodle_database.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/dml/pgsql_native_moodle_database.php,v retrieving revision 1.43 diff -u -r1.43 pgsql_native_moodle_database.php --- lib/dml/pgsql_native_moodle_database.php 3 Nov 2009 23:34:43 -0000 1.43 +++ lib/dml/pgsql_native_moodle_database.php 4 Nov 2009 09:18:24 -0000 @@ -1122,54 +1122,45 @@ /// transactions /** - * 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. - * - * this is _very_ useful for massive updates + * Driver specific start of real database transaction, + * this can not be used directly in code. + * @return void */ - public function begin_sql() { - if (!parent::begin_sql()) { - return false; - } + protected function begin_transaction() { $sql = "BEGIN ISOLATION LEVEL READ COMMITTED"; $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = pg_query($this->pgsql, $sql); $this->query_end($result); pg_free_result($result); - return true; } /** - * on DBs that support it, commit the transaction + * Driver specific commit of real database transaction, + * this can not be used directly in code. + * @return void */ - public function commit_sql() { - if (!parent::commit_sql()) { - return false; - } + protected function commit_transaction() { $sql = "COMMIT"; $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = pg_query($this->pgsql, $sql); $this->query_end($result); pg_free_result($result); - return true; } /** - * on DBs that support it, rollback the transaction + * Driver specific abort of real database transaction, + * this can not be used directly in code. + * @return void */ - public function rollback_sql() { - if (!parent::rollback_sql()) { - return false; - } + protected function rollback_transaction() { $sql = "ROLLBACK"; $this->query_start($sql, NULL, SQL_QUERY_AUX); $result = pg_query($this->pgsql, $sql); $this->query_end($result); pg_free_result($result); - return true; } /** Index: lib/dml/oci_native_moodle_database.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/dml/oci_native_moodle_database.php,v retrieving revision 1.30 diff -u -r1.30 oci_native_moodle_database.php --- lib/dml/oci_native_moodle_database.php 2 Nov 2009 14:33:46 -0000 1.30 +++ lib/dml/oci_native_moodle_database.php 4 Nov 2009 09:18:23 -0000 @@ -1527,46 +1527,35 @@ /// transactions /** - * 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. - * - * this is _very_ useful for massive updates + * Driver specific start of real database transaction, + * this can not be used directly in code. + * @return void */ - public function begin_sql() { - if (!parent::begin_sql()) { - return false; - } + protected function begin_transaction() { $this->commit_status = OCI_DEFAULT; //Done! ;-) - return true; } /** - * on DBs that support it, commit the transaction + * Driver specific commit of real database transaction, + * this can not be used directly in code. + * @return void */ - public function commit_sql() { - if (!parent::commit_sql()) { - return false; - } - + protected function commit_transaction() { $this->query_start('--oracle_commit', NULL, SQL_QUERY_AUX); $result = oci_commit($this->oci); $this->commit_status = OCI_COMMIT_ON_SUCCESS; $this->query_end($result); - return true; } /** - * on DBs that support it, rollback the transaction + * Driver specific abort of real database transaction, + * this can not be used directly in code. + * @return void */ - public function rollback_sql() { - if (!parent::rollback_sql()) { - return false; - } - + protected function rollback_transaction() { $this->query_start('--oracle_rollback', NULL, SQL_QUERY_AUX); $result = oci_rollback($this->oci); $this->commit_status = OCI_COMMIT_ON_SUCCESS; $this->query_end($result); - return true; } } Index: auth/mnet/auth.php =================================================================== RCS file: /cvsroot/moodle/moodle/auth/mnet/auth.php,v retrieving revision 1.61 diff -u -r1.61 auth.php --- auth/mnet/auth.php 1 Nov 2009 11:55:15 -0000 1.61 +++ auth/mnet/auth.php 4 Nov 2009 09:18:03 -0000 @@ -854,7 +854,7 @@ $start = ob_start(); $returnString = ''; - $DB->begin_sql(); + $transaction = $DB->start_delegated_transaction(); $useridarray = array(); foreach($array as $logEntry) { @@ -883,7 +883,7 @@ } } $MNET_REMOTE_CLIENT->commit(); - $DB->commit_sql(); + $transaction->commit(); $end = ob_end_clean(); Index: auth/db/auth.php =================================================================== RCS file: /cvsroot/moodle/moodle/auth/db/auth.php,v retrieving revision 1.41 diff -u -r1.41 auth.php --- auth/db/auth.php 1 Nov 2009 11:55:16 -0000 1.41 +++ auth/db/auth.php 4 Nov 2009 09:18:01 -0000 @@ -344,7 +344,7 @@ if (!empty($add_users)) { print_string('auth_dbuserstoadd','auth_db',count($add_users)); echo "\n"; - $DB->begin_sql(); + $transaction = $DB->start_delegated_transaction(); foreach($add_users as $user) { $username = $user; $user = $this->get_userinfo_asobj($user); @@ -375,7 +375,7 @@ echo "\t"; print_string('auth_dbinsertusererror', 'auth_db', $user->username); echo "\n"; } } - $DB->commit_sql(); + $transaction->commit(); unset($add_users); // free mem } return true; Index: lang/en_utf8/error.php =================================================================== RCS file: /cvsroot/moodle/moodle/lang/en_utf8/error.php,v retrieving revision 1.208 diff -u -r1.208 error.php --- lang/en_utf8/error.php 1 Nov 2009 09:21:41 -0000 1.208 +++ lang/en_utf8/error.php 4 Nov 2009 09:18:05 -0000 @@ -178,6 +178,7 @@ $string['ddlunknownerror'] = 'Unknown DDL library error'; $string['ddlxmlfileerror'] = 'XML database file errors found'; $string['dmlreadexception'] = 'Error reading from database'; +$string['dmltransactionexception'] = 'Database transaction error'; $string['dmlwriteexception'] = 'Error writing to database'; $string['destinationcmnotexit'] = 'The destination course module does not exist'; $string['detectedbrokenplugin'] = 'Plugin \"$a\" is defective, can not continue, sorry.'; Index: lib/simpletest/filtersettingsperformancetester.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/simpletest/filtersettingsperformancetester.php,v retrieving revision 1.7 diff -u -r1.7 filtersettingsperformancetester.php --- lib/simpletest/filtersettingsperformancetester.php 1 Nov 2009 13:13:21 -0000 1.7 +++ lib/simpletest/filtersettingsperformancetester.php 4 Nov 2009 09:18:27 -0000 @@ -185,7 +185,7 @@ // Activities contexts. $mods = array(); $prog = new progress_bar('modbar', 500, true); - $DB->begin_sql(); + $transaction = $DB->start_delegated_transaction(); for ($i = 0; $i < $nummodules; $i++) { $context = insert_context(CONTEXT_MODULE, $i, $courses[array_rand($courses)]); $mods[$context->id] = $context; @@ -193,7 +193,7 @@ $prog->update($i, $nummodules, ''); } } - $DB->commit_sql(); + $transaction->commit(); echo $OUTPUT->notification('Created ' . $nummodules . ' module contexts.', 'notifysuccess'); flush(); $contexts = $categories + $courses + $mods; @@ -212,20 +212,20 @@ // Local overrides. $localstates = array(TEXTFILTER_OFF => 0, TEXTFILTER_ON => 0); $prog = new progress_bar('locbar', 500, true); - $DB->begin_sql(); + $transaction = $DB->start_delegated_transaction(); for ($i = 0; $i < $numoverrides; $i++) { filter_set_local_state(array_rand($installedfilters), array_rand($contexts), array_rand($localstates)); if ($i % 50) { $prog->update($i, $numoverrides, ''); } } - $DB->commit_sql(); + $transaction->commit(); echo $OUTPUT->notification('Set ' . $numoverrides . ' local overrides.', 'notifysuccess'); flush(); // Local config. $variablenames = array('frog' => 0, 'toad' => 0, 'elver' => 0, 'eft' => 0, 'tadpole' => 0); $prog = new progress_bar('confbar', 500, true); - $DB->begin_sql(); + $transaction = $DB->start_delegated_transaction(); for ($i = 0; $i < $numconfigs; $i++) { filter_set_local_config(array_rand($installedfilters), array_rand($contexts), array_rand($variablenames), random_string(rand(20, 40))); @@ -233,7 +233,7 @@ $prog->update($i, $numconfigs, ''); } } - $DB->commit_sql(); + $transaction->commit(); echo $OUTPUT->notification('Set ' . $numconfigs . ' local configs.', 'notifysuccess'); flush(); } Index: auth/ldap/auth.php =================================================================== RCS file: /cvsroot/moodle/moodle/auth/ldap/auth.php,v retrieving revision 1.79 diff -u -r1.79 auth.php --- auth/ldap/auth.php 1 Nov 2009 11:55:15 -0000 1.79 +++ auth/ldap/auth.php 4 Nov 2009 09:18:02 -0000 @@ -729,7 +729,7 @@ $creatorrole = false; } - $DB->begin_sql(); + $transaction = $DB->start_delegated_transaction(); $xcount = 0; $maxxcount = 100; @@ -749,14 +749,8 @@ role_unassign($creatorrole->id, $user->id, 0, $sitecontext->id, 'ldap'); } } - - if ($xcount++ > $maxxcount) { - $DB->commit_sql(); - $DB->begin_sql(); - $xcount = 0; - } } - $DB->commit_sql(); + $transaction->commit(); unset($users); // free mem } } else { // end do updates @@ -785,7 +779,7 @@ $creatorrole = false; } - $DB->begin_sql(); + $transaction = $DB->start_delegated_transaction(); foreach ($add_users as $user) { $user = $this->get_userinfo_asobj($user->username); @@ -816,7 +810,7 @@ role_assign($creatorrole->id, $user->id, 0, $sitecontext->id, 0, 0, 0, 'ldap'); } } - $DB->commit_sql(); + $transaction->commit(); unset($add_users); // free mem } else { print "No users to be added\n"; Index: auth/cas/auth.php =================================================================== RCS file: /cvsroot/moodle/moodle/auth/cas/auth.php,v retrieving revision 1.38 diff -u -r1.38 auth.php --- auth/cas/auth.php 1 Nov 2009 11:55:16 -0000 1.38 +++ auth/cas/auth.php 4 Nov 2009 09:18:00 -0000 @@ -796,7 +796,7 @@ $creatorrole = false; } - $DB->begin_sql(); + $transaction = $DB->start_delegated_transaction(); $xcount = 0; $maxxcount = 100; @@ -816,14 +816,8 @@ role_unassign($creatorrole->id, $user->id, 0, $sitecontext->id, 'cas'); } } - - if ($xcount++ > $maxxcount) { - $DB->commit_sql(); - $DB->begin_sql(); - $xcount = 0; - } } - $DB->commit_sql(); + $transaction->commit(); unset($users); // free mem } } else { // end do updates @@ -851,7 +845,7 @@ $creatorrole = false; } - $DB->begin_sql(); + $transaction = $DB->start_delegated_transaction(); foreach ($add_users as $user) { $user = $this->get_userinfo_asobj($user->username); @@ -879,7 +873,7 @@ role_assign($creatorrole->id, $user->id, 0, $sitecontext->id, 0, 0, 0, 'cas'); } } - $DB->commit_sql(); + $transaction->commit(); unset($add_users); // free mem } else { print "No users to be added\n"; Index: lib/dml/moodle_transaction.php =================================================================== RCS file: lib/dml/moodle_transaction.php diff -N lib/dml/moodle_transaction.php --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ lib/dml/moodle_transaction.php 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,93 @@ +. + + +/** + * Delegated database transaction support. + * + * @package moodlecore + * @subpackage DML + * @copyright 2009 Petr Skoda (http://skodak.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Delegated transaction class. + */ +class moodle_transaction { + private $start_backtrace; + private $database = null; + + /** + * Delegated transaction constructor, + * can be called only from moodle_database class. + * Unfortunately PHP's protected keyword is useless. + * @param moodle_database $database + */ + public function __construct($database) { + $this->database = $database; + $this->start_backtrace = debug_backtrace(); + } + + /** + * Is the delegated transaction already used? + * @return bool true if commit and rollback allowed, false if already done + */ + public function is_disposed() { + return empty($this->database); + } + + /** + * Mark transaction as disposed, no more + * commits and rollbacks allowed. + * To be used only from moodle_database class + * @return unknown_type + */ + public function dispose() { + return $this->database = null; + } + + /** + * Commit delegated transaction. + * The real database commit SQL is executed + * only after commiting all delegated transactions. + * + * Incorrect order of nested commits or rollback + * at any level is resulting in rollback of SQL transaction. + * + * @return void + */ + public function commit() { + if ($this->is_disposed()) { + throw new dml_transaction_exception('Transactions already disposed', $this); + } + $this->database->commit_delegated_transaction($this); + } + + /** + * Rollback all current delegated transactions. + * + * @param Exception $e mandatory exception + * @return void + */ + public function rollback(Exception $e) { + if ($this->is_disposed()) { + throw new dml_transaction_exception('Transactions already disposed', $this); + } + $this->database->rollback_delegated_transaction($this, $e); + } +} \ No newline at end of file