]> git.mjollnir.org Git - moodle.git/commitdiff
MDL-20625 new delegated transaction support in DML
authorPetr Skoda <skodak@moodle.org>
Sat, 7 Nov 2009 08:52:56 +0000 (08:52 +0000)
committerPetr Skoda <skodak@moodle.org>
Sat, 7 Nov 2009 08:52:56 +0000 (08:52 +0000)
25 files changed:
auth/cas/auth.php
auth/db/auth.php
auth/ldap/auth.php
auth/mnet/auth.php
enrol/database/enrol.php
group/delete.php
group/externallib.php
lang/en_utf8/error.php
lib/accesslib.php
lib/dml/moodle_database.php
lib/dml/moodle_transaction.php [new file with mode: 0644]
lib/dml/mssql_native_moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/oci_native_moodle_database.php
lib/dml/pdo_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/simpletest/testdml.php
lib/dmllib.php
lib/dtl/database_importer.php
lib/moodlelib.php
lib/outputlib.php
lib/outputrenderers.php
lib/setuplib.php
lib/simpletest/filtersettingsperformancetester.php
mod/quiz/locallib.php

index d9ec82969f6c399d9ca7f092168fc8d32ff25a52..2e65812baf7536d67af077514eb4b13e47e6e8c2 100644 (file)
@@ -796,7 +796,7 @@ if ( !is_object($PHPCAS_CLIENT) ) {
                     $creatorrole = false;
                 }
 
-                $DB->begin_sql();
+                $transaction = $DB->start_delegated_transaction();
                 $xcount = 0;
                 $maxxcount = 100;
 
@@ -816,14 +816,8 @@ if ( !is_object($PHPCAS_CLIENT) ) {
                             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->allow_commit();
                 unset($users); // free mem
             }
         } else { // end do updates
@@ -851,7 +845,7 @@ if ( !is_object($PHPCAS_CLIENT) ) {
                 $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 @@ if ( !is_object($PHPCAS_CLIENT) ) {
                     role_assign($creatorrole->id, $user->id, 0, $sitecontext->id, 0, 0, 0, 'cas');
                 }
             }
-            $DB->commit_sql();
+            $transaction->allow_commit();
             unset($add_users); // free mem
         } else {
             print "No users to be added\n";
index 4cc1865ba44b4b5fa9c25f607d04e63705c3c60d..724496cef9d0fe3a6ba931e2b9ac7129446b5892 100644 (file)
@@ -344,7 +344,7 @@ class auth_plugin_db extends auth_plugin_base {
 
         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 @@ class auth_plugin_db extends auth_plugin_base {
                     echo "\t"; print_string('auth_dbinsertusererror', 'auth_db', $user->username); echo "\n";
                 }
             }
-            $DB->commit_sql();
+            $transaction->allow_commit();
             unset($add_users); // free mem
         }
         return true;
index bc65003facc00207a3ca313b1d51d5208913084f..ca3a7746d5ce31675f25057b13a191f44b6e491c 100644 (file)
@@ -729,7 +729,7 @@ class auth_plugin_ldap extends auth_plugin_base {
                     $creatorrole = false;
                 }
 
-                $DB->begin_sql();
+                $transaction = $DB->start_delegated_transaction();
                 $xcount = 0;
                 $maxxcount = 100;
 
@@ -749,14 +749,8 @@ class auth_plugin_ldap extends auth_plugin_base {
                             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->allow_commit();
                 unset($users); // free mem
             }
         } else { // end do updates
@@ -785,7 +779,7 @@ class auth_plugin_ldap extends auth_plugin_base {
                 $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 @@ class auth_plugin_ldap extends auth_plugin_base {
                     role_assign($creatorrole->id, $user->id, 0, $sitecontext->id, 0, 0, 0, 'ldap');
                 }
             }
-            $DB->commit_sql();
+            $transaction->allow_commit();
             unset($add_users); // free mem
         } else {
             print "No users to be added\n";
index c3a4f999ad78c1e6a7a362cbb847ee064e11fc7d..ddae159e146a9f70f5fe02128e9adc614abb0b89 100644 (file)
@@ -854,7 +854,7 @@ class auth_plugin_mnet extends auth_plugin_base {
         $start = ob_start();
 
         $returnString = '';
-        $DB->begin_sql();
+        $transaction = $DB->start_delegated_transaction();
         $useridarray = array();
 
         foreach($array as $logEntry) {
@@ -883,7 +883,7 @@ class auth_plugin_mnet extends auth_plugin_base {
             }
         }
         $MNET_REMOTE_CLIENT->commit();
-        $DB->commit_sql();
+        $transaction->allow_commit();
 
         $end = ob_end_clean();
 
index 0665ab713b40080ea1dd36f3cbcd4559fbea835c..4df2185a6884b96d24c30484e6643de59340cb5e 100644 (file)
@@ -219,7 +219,8 @@ function sync_enrolments($role = null) {
         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 @@ function sync_enrolments($role = null) {
         $ers->close(); // release the handle
     }
 
-    $DB->commit_sql();
+    $transaction->allow_commit();
 
     // we are done now, a bit of housekeeping
     fix_course_sortorder();
index db0e2e4adbeb7c3e856a3f46997a46ad1738c57f..7fa0ec66ec790a145d667fa80950d73e214f929c 100644 (file)
@@ -49,11 +49,11 @@ if ($confirm && data_submitted()) {
     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 18d81fdf04ac1b69d57bd463566ee540ccc36a68..74ac1e4fa35903db18167e31fb34e1a665fd5362 100644 (file)
@@ -60,36 +60,31 @@ class moodle_group_external extends external_api {
 
         $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->allow_commit();
 
         return $groups;
     }
@@ -242,30 +237,27 @@ class moodle_group_external extends external_api {
 
         $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->allow_commit();
     }
 
    /**
@@ -361,33 +353,29 @@ class moodle_group_external extends external_api {
 
         $params = self::validate_parameters(self::add_groupmembers_parameters(), array('members'=>$members));
 
-        $DB->begin_sql();
-        try {
-// 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'];
+        $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);
+            $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);
+            // now security checks
+            $context = get_context_instance(CONTEXT_COURSE, $group->courseid);
+            self::validate_context($context);
+            require_capability('moodle/course:managegroups', $context);
 
-                // now make sure user is enrolled in course - this is mandatory requirement,
-                // unfortunately this is extermely slow
-                require_capability('moodle/course:view', $context, $userid, false);
+            // now make sure user is enrolled in course - this is mandatory requirement,
+            // unfortunately this is extermely slow
+            require_capability('moodle/course:view', $context, $userid, false);
 
-                groups_add_member($group, $user);
-            }
-        } catch (Exception $ex) {
-            $DB->rollback_sql();
-            throw $ex;
+            groups_add_member($group, $user);
         }
-        $DB->commit_sql();
+
+        $transaction->allow_commit();
     }
 
    /**
@@ -429,29 +417,26 @@ class moodle_group_external extends external_api {
 
         $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'];
+            // 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);
+            $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);
+            // 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);
-            }
-        } catch (Exception $ex) {
-            $DB->rollback_sql();
-            throw $ex;
+            groups_remove_member($group, $user);
         }
-        $DB->commit_sql();
+
+        $transaction->allow_commit();
     }
 
    /**
index 7ce6f43d55b734c3696a9eecf7108ea0e61238b7..e38aa7a1d0468707a6a27cac3e6bec4dbba9516b 100644 (file)
@@ -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['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 d4a03108b8367595a1ad83fbe3fafb9d92636a4d..83a5243c85cd5805a685acb201c144b165fae464 100755 (executable)
@@ -2455,24 +2455,18 @@ function cleanup_contexts() {
                      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->allow_commit();
     return true;
 }
 
index 4dff70cb424ee10aecc4328b883cee3d1ee4dcfe..0e178d080e002f045af2c1f124f9528b277a9a36 100644 (file)
@@ -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 @@ abstract class moodle_database {
     /** @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 @@ abstract class moodle_database {
      * 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) {
@@ -1883,59 +1888,181 @@ abstract class moodle_database {
     }
 
 /// transactions
+
+    /**
+     * Are transactions supported?
+     * It is not responsible to run productions servers
+     * on databases without transaction support ;-)
+     *
+     * Override in driver if needed.
+     *
+     * @return bool
+     */
+    protected function transactions_supported() {
+        // protected for now, this might be changed to public if really necessary
+        return true;
+    }
+
     /**
      * Returns true if transaction in progress
      * @return bool
      */
-    function is_transaction_started() {
-        return $this->intransaction;
+    public function is_transaction_started() {
+        return !empty($this->transactions);
+    }
+
+    /**
+     * Throws exception if transaction in progress.
+     * This test does not force rollback of active transactions.
+     * @return void
+     */
+    public function transactions_forbidden() {
+        if ($this->is_transaction_started()) {
+            throw new dml_transaction_exception('This code can not be excecuted in 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.
+     * 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.
-     *
-     * @return bool success
+     * 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 moodle_transaction
+     */
+    public function start_delegated_transaction() {
+        $transaction = new moodle_transaction($this);
+        $this->transactions[] = $transaction;
+        if (count($this->transactions) == 1) {
+            $this->begin_transaction();
+        }
+        return $transaction;
+    }
+
+    /**
+     * Driver specific start of real database transaction,
+     * this can not be used directly in code.
+     * @return void
      */
-    public function begin_sql() {
-        if ($this->intransaction) {
-            debugging('Transaction already 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 = true;
-        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, commit the transaction
-     * @return bool success
+     * Driver specific commit 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 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;
     }
 
     /**
-     * on DBs that support it, rollback the transaction
-     * @return bool success
+     * Driver specific bort 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 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
+            }
         }
-        $this->intransaction = false;
-        return true;
+
+        // now enable transactions again
+        $this->transactions = array(); // unfortunately all unfinished exceptions are kept in memory
+        $this->force_rollback = false;
     }
 
 /// session locking
diff --git a/lib/dml/moodle_transaction.php b/lib/dml/moodle_transaction.php
new file mode 100644 (file)
index 0000000..37d252e
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+
+/**
+ * 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 allow_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
index 07398f53c7b15e325c4a6c51e1ce81b55eacd846..b7d5abfc33dae72766e2be9b2c7ef6d122b821b5 100644 (file)
@@ -1199,56 +1199,44 @@ s only returning name of SQL substring function, it now requires all parameters.
 /// 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 2ba86cd90eb9bacac48a908843b3ae4faa3f7d4e..68bf10f497a7d6c36883f4eaf5c5a3c7d9913619 100644 (file)
@@ -37,6 +37,8 @@ class mysqli_native_moodle_database extends moodle_database {
     protected $mysqli = null;
     private $temptables; // Control existing temptables (mysql_moodle_temptables object)
 
+    private $transactions_supported = null;
+
     /**
      * Attempt to create the database
      * @param string $dbhost
@@ -1039,28 +1041,45 @@ class mysqli_native_moodle_database extends moodle_database {
 
 /// 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.
+     * Are transactions supported?
+     * It is not responsible to run productions servers
+     * on databases without transaction support ;-)
+     *
+     * MyISAM does not support support transactions.
      *
-     * this is _very_ useful for massive updates
+     * @return bool
      */
-    public function begin_sql() {
+    protected function transactions_supported() {
+        if (!is_null($this->transactions_supported)) {
+            return $this->transactions_supported;
+        }
+
         // Only will accept transactions if using InnoDB storage engine (more engines can be added easily BDB, Falcon...)
+        $this->transactions_supported = false;
+
         $sql = "SELECT @@storage_engine";
         $this->query_start($sql, NULL, SQL_QUERY_AUX);
         $result = $this->mysqli->query($sql);
         $this->query_end($result);
+
         if ($rec = $result->fetch_assoc()) {
-            if (!in_array($rec['@@storage_engine'], array('InnoDB'))) {
-                return false;
+            if (in_array($rec['@@storage_engine'], array('InnoDB'))) {
+                $this->transactions_supported = true;
             }
-        } else {
-            return false;
         }
         $result->close();
 
-        if (!parent::begin_sql()) {
-            return false;
+        return $this->transactions_supported;
+    }
+
+    /**
+     * Driver specific start of real database transaction,
+     * this can not be used directly in code.
+     * @return void
+     */
+    protected function begin_transaction() {
+        if (!$this->transactions_supported()) {
+            return;
         }
 
         $sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED";
@@ -1072,32 +1091,34 @@ class mysqli_native_moodle_database extends moodle_database {
         $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() {
+        if (!$this->transactions_supported()) {
+            return;
         }
+
         $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() {
-        if (!parent::rollback_sql()) {
-            return false;
+    protected function rollback_transaction() {
+        if (!$this->transactions_supported()) {
+            return;
         }
+
         $sql = "ROLLBACK";
         $this->query_start($sql, NULL, SQL_QUERY_AUX);
         $result = $this->mysqli->query($sql);
index 16b3a4203758ece2074305555129b080a3318bb8..e9c5c408fe51fcb1f24c20f7b1e8ecc315643001 100644 (file)
@@ -1527,46 +1527,35 @@ class oci_native_moodle_database extends moodle_database {
 
 /// 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 bcd79a70e27ac7ecd9bb2b164b69e33df6f6a3b4..86fdb544ebc629ec3f87bb3ee742b41d7bd42fb6 100644 (file)
@@ -542,57 +542,36 @@ abstract class pdo_moodle_database extends moodle_database {
         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 6e9836f41cb14a23f5604eb2621f72fb68b9efe6..605e7286fd5151008d7051b32318e47820d1c92b 100644 (file)
@@ -1122,54 +1122,45 @@ class pgsql_native_moodle_database extends moodle_database {
 
 /// 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 56a90c9b986a7920f63bc4778eb70ddce746572b..2ed566c528b9b8eb3e26c2c48570c59b8fa4e98d 100755 (executable)
@@ -2297,7 +2297,12 @@ class dml_test extends UnitTestCase {
 
     }
 
-    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,75 @@ class dml_test extends UnitTestCase {
         $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_onelevel_commit() {
+        $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;
+
+        $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->allow_commit();
+        $this->assertEqual(1, $DB->count_records($tablename));
+    }
+
+    function test_onelevel_rollback() {
+        $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;
+
+        // 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));
     }
 
-    function test_commit_sql() {
+    function test_nested_transactions() {
         $DB = $this->tdb;
         $dbman = $DB->get_manager();
 
@@ -2335,19 +2396,72 @@ class dml_test extends UnitTestCase {
         $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');
+        // 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->allow_commit();
+        $this->assertTrue($DB->is_transaction_started());
+        $transaction1->allow_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->allow_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));
+
+        $DB->delete_records($tablename);
+
+        // 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->allow_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_rollback_sql() {
+    function test_transactions_forbidden() {
         $DB = $this->tdb;
         $dbman = $DB->get_manager();
 
@@ -2360,24 +2474,22 @@ class dml_test extends UnitTestCase {
         $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');
+        $DB->transactions_forbidden();
+        $transaction = $DB->start_delegated_transaction();
+        $data = (object)array('course'=>1);
+        $DB->insert_record($tablename, $data);
+        try {
+            $DB->transactions_forbidden();
+        } catch (Exception $e) {
+            $this->assertEqual(get_class($e), 'dml_transaction_exception');
         }
+        // the previous test does not force rollback
+        $transaction->allow_commit();
+        $this->assertFalse($DB->is_transaction_started());
+        $this->assertEqual(1, $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_wrong_transactions() {
         $DB = $this->tdb;
         $dbman = $DB->get_manager();
 
@@ -2390,22 +2502,83 @@ class dml_test extends UnitTestCase {
         $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));
 
-        // 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 = ?';
+        // 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->allow_commit();
+            $this->fail('wrong order of commits must throw exception');
+        } catch (Exception $e) {
+            $this->assertEqual(get_class($e), 'dml_transaction_exception');
+        }
+        try {
+            $transaction2->allow_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);
 
-        $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);
+
+        // 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->allow_commit();
+            $this->fail('foreign transaction must fail');
+        } catch (Exception $e) {
+            $this->assertEqual(get_class($e), 'dml_transaction_exception');
+        }
+        try {
+            $transaction1->allow_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);
     }
 }
 
@@ -2454,4 +2627,7 @@ class moodle_database_for_testing extends moodle_database {
     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 a4a2fbc99a000d8611cfbd5f5a95fd7399947823..e418d37c2efc80f7b731543ae6c7cc86c660ce4f 100644 (file)
@@ -196,6 +196,23 @@ class dml_write_exception extends dml_exception {
     }
 }
 
+/**
+ * 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
  *
index 13a1424981eec53d79da215cd9f53af7ca5ef967..5db29ea36a234041176fa3f6504d23034567bd53 100644 (file)
@@ -57,6 +57,8 @@ class database_importer {
      * How to use transactions.
      */
     protected $transactionmode = 'allinone';
+    /** Transaction object */
+    protected $transaction;
 
     /**
      * Object constructor.
@@ -123,7 +125,7 @@ class database_importer {
             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 @@ class database_importer {
      */
     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 @@ class database_importer {
             }
         }
         if ($this->transactionmode == 'pertable') {
-            $this->mdb->commit_sql();
+            $this->transaction->allow_commit();
         }
     }
 
@@ -182,7 +184,7 @@ class database_importer {
      */
     public function finish_database_import() {
         if ($this->transactionmode == 'allinone') {
-            $this->mdb->commit_sql();
+            $this->transaction->allow_commit();
         }
     }
 
index ae2c584155c67db9508ccd697f33a9b73d8430b1..64a8ad3472e192b409fb85995b359019afa2a45c 100644 (file)
@@ -3392,61 +3392,50 @@ function delete_user($user) {
     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 6a928530520055ab8637e3db57256eab71f26ae4..14dec6e4d5c41b87fa7731eeb6d40a7a57aee497 100644 (file)
@@ -859,6 +859,9 @@ class xhtml_container_stack {
             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 bb7b8d9ac2882cd1bd1f755c32b6f50eeeae0b61..993b3dc189db77c91715c07c791c76382320415f 100644 (file)
@@ -874,12 +874,16 @@ class moodle_core_renderer extends moodle_renderer_base {
      * @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 f2290db03cedea17fa33e134b2b0d2163eaaf9e0..ba1c48e76682dc233edb991e60f550246001b869 100644 (file)
@@ -208,14 +208,12 @@ function default_exception_handler($ex) {
 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 87e35fca7533a01abe38231e8b755239263323c9..c6bb20baf35aa328af424f77a84883b61fcab386 100644 (file)
@@ -185,7 +185,7 @@ function populate_test_database($syscontext, $numcategories, $numcourses, $nummo
     // 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 @@ function populate_test_database($syscontext, $numcategories, $numcourses, $nummo
             $prog->update($i, $nummodules, '');
         }
     }
-    $DB->commit_sql();
+    $transaction->allow_commit();
     echo $OUTPUT->notification('Created ' . $nummodules . ' module contexts.', 'notifysuccess'); flush();
 
     $contexts = $categories + $courses + $mods;
@@ -212,20 +212,20 @@ function populate_test_database($syscontext, $numcategories, $numcourses, $nummo
     // 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->allow_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 @@ function populate_test_database($syscontext, $numcategories, $numcourses, $nummo
             $prog->update($i, $numconfigs, '');
         }
     }
-    $DB->commit_sql();
+    $transaction->allow_commit();
     echo $OUTPUT->notification('Set ' . $numconfigs . ' local configs.', 'notifysuccess'); flush();
 }
 
index 3bbb164b753e6f5fde25e43054aee8d274a84050..209d69fbe1a7be1f9af9e10e155931a6a30ba9c8 100644 (file)
@@ -496,7 +496,7 @@ function quiz_has_feedback($quiz) {
  *
  * @param float $newgrade the new maximum grade for the quiz.
  * @param object $quiz the quiz we are updating. Passed by reference so its grade field can be updated too.
- * @return boolean indicating success or failure.
+ * @return boolean indicating success or failure. TODO: MDL-20625
  */
 function quiz_set_grade($newgrade, &$quiz) {
     global $DB;
@@ -507,43 +507,45 @@ function quiz_set_grade($newgrade, &$quiz) {
     }
 
     // Use a transaction, so that on those databases that support it, this is safer.
-    $DB->begin_sql();
-
-    // Update the quiz table.
-    $success = $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();
-        $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));
-    }
+    $transaction = $DB->start_delegated_transaction();
+
+    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));
+        }
 
-    // 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();
-        return false;
+        $transaction->allow_commit();
+        return true;
+
+    } catch (Exception $e) {
+        //TODO: MDL-20625 this part was returning false, but now throws exception
+        $transaction->rollback($e);
     }
 }