Index: includes/database/database.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/database/database.inc,v
retrieving revision 1.97
diff -u -p -r1.97 database.inc
--- includes/database/database.inc	15 Feb 2010 22:12:27 -0000	1.97
+++ includes/database/database.inc	26 Feb 2010 02:42:11 -0000
@@ -208,9 +208,9 @@ abstract class DatabaseConnection extend
    * nested calls to transactions and collapse them into a single
    * transaction.
    *
-   * @var int
+   * @var array
    */
-  protected $transactionLayers = 0;
+  protected $transactionLayers = array();
 
   /**
    * Whether or not the active transaction (if any) will be rolled back.
@@ -845,7 +845,14 @@ abstract class DatabaseConnection extend
    *   TRUE if we're currently in a transaction, FALSE otherwise.
    */
   public function inTransaction() {
-    return ($this->transactionLayers > 0);
+    return (count($this->transactionLayers) > 0);
+  }
+
+  /**
+   * Determine transaction depth
+   */
+  public function transactionDepth() {
+    return count($this->transactionLayers);
   }
 
   /**
@@ -853,21 +860,23 @@ abstract class DatabaseConnection extend
    *
    * @see DatabaseTransaction
    */
-  public function startTransaction() {
+  public function startTransaction($name = '') {
     if (empty($this->transactionClass)) {
       $this->transactionClass = 'DatabaseTransaction_' . $this->driver();
       if (!class_exists($this->transactionClass)) {
         $this->transactionClass = 'DatabaseTransaction';
       }
     }
-    return new $this->transactionClass($this);
+    return new $this->transactionClass($this, $name);
   }
 
   /**
-   * Schedule the current transaction for rollback.
+   * Rollback transaction or to savepoint defined by $name.
    *
    * This method throws an exception if no transaction is active.
    *
+   * @param $name
+   *   The name of the savepoint or transaction.
    * @param $type
    *   The category to which the rollback message belongs.
    * @param $message
@@ -887,11 +896,15 @@ abstract class DatabaseConnection extend
    * @see DatabaseTransaction::rollback()
    * @see watchdog()
    */
-  public function rollback($type = NULL, $message = NULL, $variables = array(), $severity = NULL, $link = NULL) {
-    if ($this->transactionLayers == 0) {
-      throw new NoActiveTransactionException();
+  public function rollback($savepoint_name, $type = NULL, $message = NULL, $variables = array(), $severity = NULL, $link = NULL, $name = NULL) {
+    if (!$this->transactionDepth()) {
+      throw new  NoActiveTransactionException();
+    }
+    // A previous rollback to an earlier savepoint may mean that the savepoint
+    // in question has already been rolled back.
+    if (!in_array($savepoint_name, $this->transactionLayers)) {
+      return;
     }
-
     // Set the severity to the configured default if not specified.
     if (!isset($severity)) {
       $logging = Database::getLoggingCallback();
@@ -915,25 +928,46 @@ abstract class DatabaseConnection extend
       );
     }
 
-    $this->willRollback = TRUE;
+    // We need to find the point we're rolling back to, all other savepoints
+    // before are no longer needed.
+    while ($savepoint = array_pop($this->transactionLayers)) {
+      if ($savepoint == $savepoint_name) {
+        // If it is the last the transaction in the stack, then it is not a
+        // savepoint, it is the transaction itself so we will need to rollback
+        // the transaction rather than a savepoint.
+        if (empty($this->transactionLayers)) {
+          break;
+        }
+        $this->query('ROLLBACK TO SAVEPOINT ' . $savepoint);
+        return;
+      }
+    }
+    if ($this->supportsTransactions()) {
+      parent::rollBack();
+    }
+    $this->logRollback();
   }
 
-  /**
-   * Determine if this transaction will roll back.
-   *
-   * Use this function to skip further operations if the current transaction
-   * is already scheduled to roll back. Throws an exception if no transaction
-   * is active.
-   *
-   * @return
-   *   TRUE if the transaction will roll back, FALSE otherwise.
-   */
-  public function willRollback() {
-    if ($this->transactionLayers == 0) {
-      throw new NoActiveTransactionException();
+  protected function logRollback() {
+    $logging = Database::getLoggingCallback();
+    // If there is no callback defined. We can't do anything.
+    if (!is_array($logging)) {
+      return;
+    }
+
+    $logging_callback = $logging['callback'];
+
+    // Log the failed rollback.
+    $logging_callback('database', 'Explicit rollback failed: not supported on active connection.', array(), $logging['error_severity']);
+
+    // Play back the logged errors to the specified logging callback post-
+    // rollback.
+    foreach ($this->rollbackLogs as $log_item) {
+      $logging_callback($log_item['type'], $log_item['message'], $log_item['variables'], $log_item['severity'], $log_item['link']);
     }
 
-    return $this->willRollback;
+    // Reset the error logs.
+    $this->rollbackLogs = array();
   }
 
   /**
@@ -943,14 +977,22 @@ abstract class DatabaseConnection extend
    *
    * @see DatabaseTransaction
    */
-  public function pushTransaction() {
-    ++$this->transactionLayers;
-
-    if ($this->transactionLayers == 1) {
-      if ($this->supportsTransactions()) {
-        parent::beginTransaction();
-      }
+  public function pushTransaction($name) {
+    if (!$this->supportsTransactions()) {
+      return;
+    }
+    if (isset($this->transactionLayers[$name])) {
+      throw new NonUniqueTransactionNameException($name . " is already in use.");
+    }
+    // If we're already in a transaction then we want to create a savepoint
+    // rather than try to create another transaction.
+    if ($this->transactionDepth()) {
+      $this->query('SAVEPOINT ' . $name);
     }
+    else {
+      parent::beginTransaction();
+    }
+    $this->transactionLayers[$name] = $name;
   }
 
   /**
@@ -958,56 +1000,32 @@ abstract class DatabaseConnection extend
    * necessary.
    *
    * If we pop off the last transaction layer, then we either commit or roll
-   * back the transaction as necessary.  If no transaction is active, we throw
-   * an exception.
+   * back the transaction as necessary.  If no transaction is active, we return
+   * because the transaction may have manually been rolled back.
    *
    * @see DatabaseTransaction
    */
-  public function popTransaction() {
-    if ($this->transactionLayers == 0) {
+  public function popTransaction($name) {
+    if (!$this->supportsTransactions()) {
+      return;
+    }
+    if (!$this->transactionDepth()) {
       throw new NoActiveTransactionException();
     }
 
-    --$this->transactionLayers;
-
-    if ($this->transactionLayers == 0) {
-      if ($this->willRollback) {
-        // Reset the rollback status so that the next transaction starts clean.
-        $this->willRollback = FALSE;
-
-        // Reset the error log.
-        $rollback_logs = $this->rollbackLogs;
-        $this->rollbackLogs = array();
-
-        $logging = Database::getLoggingCallback();
-        $logging_callback = NULL;
-        if (is_array($logging)) {
-          $logging_callback = $logging['callback'];
-        }
-
-        if ($this->supportsTransactions()) {
-          parent::rollBack();
-        }
-        else {
-          if (isset($logging_callback)) {
-            // Log the failed rollback.
-            $logging_callback('database', 'Explicit rollback failed: not supported on active connection.', array(), $logging['error_severity']);
-          }
-
-          // It would be nice to throw an exception here if logging failed,
-          // but throwing exceptions in destructors is not supported.
-        }
-
-        if (isset($logging_callback)) {
-          // Play back the logged errors to the specified logging callback post-
-          // rollback.
-          foreach ($rollback_logs as $log_item) {
-            $logging_callback($log_item['type'], $log_item['message'], $log_item['variables'], $log_item['severity'], $log_item['link']);
-          }
+    // Commit everything since SAVEPOINT $name
+    while($savepoint = array_pop($this->transactionLayers)) {
+      if ($savepoint != $name) continue;
+
+      // If there are no more layers left then we should commit.
+      if (empty($this->transactionLayers)) {
+        if (!parent::commit()) {
+          throw new TransactionCommitFailedException();
         }
       }
-      elseif ($this->supportsTransactions()) {
-        parent::commit();
+      else {
+        $this->query('RELEASE SAVEPOINT ' . $name);
+        break;
       }
     }
   }
@@ -1577,6 +1595,16 @@ abstract class Database {
 class NoActiveTransactionException extends Exception { }
 
 /**
+ * Exception to throw when a savepoint or transaction name occurs twice.
+ */
+class NonUnqiueTransactionNameException extends Exception { }
+
+/**
+ * Exception to throw when commit function fails.
+ */
+class TransactionCommitFailedException extends Exception { }
+
+/**
  * Exception to deny attempts to explicitly manage transactions.
  *
  * This exception will be thrown when the PDO connection commit() is called.
@@ -1632,13 +1660,42 @@ class DatabaseTransaction {
    */
   protected $connection;
 
-  public function __construct(DatabaseConnection &$connection) {
+  /**
+   * A boolean value to indicate whether this transaction has been rolled back.
+   *
+   * @var Boolean
+   */
+  protected $has_rolled_back = FALSE;
+
+  /**
+   * The name of the transaction.
+   */
+  protected $name;
+
+  public function __construct(DatabaseConnection &$connection, $name = NULL) {
     $this->connection = &$connection;
-    $this->connection->pushTransaction();
+    if (!$name) {
+      $depth = $connection->transactionDepth();
+      $this->name = $depth ? 'savepoint_' . $depth : 'drupal_transaction';
+    }
+    else {
+      $this->name = $name;
+    }
+    $this->connection->pushTransaction($this->name);
   }
 
   public function __destruct() {
-    $this->connection->popTransaction();
+    // If we rolled back then the transaction would have already been popped.
+    if ($this->connection->transactionDepth() && !$this->has_rolled_back) {
+      $this->connection->popTransaction($this->name);
+    }
+  }
+
+  /**
+   * Retrive the name of the transaction or savepoint.
+   */
+  public function name() {
+    return $this->name;
   }
 
   /**
@@ -1666,22 +1723,15 @@ class DatabaseTransaction {
    * @see watchdog()
    */
   public function rollback($type = NULL, $message = NULL, $variables = array(), $severity = NULL, $link = NULL) {
+    $this->has_rolled_back = TRUE;
     if (!isset($severity)) {
       $logging = Database::getLoggingCallback();
       if (is_array($logging)) {
         $severity = $logging['default_severity'];
       }
     }
-    $this->connection->rollback($type, $message, $variables, $severity, $link);
+    $this->connection->rollback($this->name, $type, $message, $variables, $severity, $link);
   }
-
-  /**
-   * Determine if this transaction will roll back.
-   */
-  public function willRollback() {
-    return $this->connection->willRollback();
-  }
-
 }
 
 /**
@@ -2266,11 +2316,11 @@ function db_select($table, $alias = NULL
  * @return DatabaseTransaction
  *   A new DatabaseTransaction object for this connection.
  */
-function db_transaction($required = FALSE, Array $options = array()) {
+function db_transaction($name = NULL, $required = FALSE, Array $options = array()) {
   if (empty($options['target'])) {
     $options['target'] = 'default';
   }
-  return Database::getConnection($options['target'])->startTransaction($required);
+  return Database::getConnection($options['target'])->startTransaction($name, $required);
 }
 
 /**
Index: modules/simpletest/tests/database_test.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/database_test.test,v
retrieving revision 1.81
diff -u -p -r1.81 database_test.test
--- modules/simpletest/tests/database_test.test	17 Feb 2010 05:24:53 -0000	1.81
+++ modules/simpletest/tests/database_test.test	26 Feb 2010 02:42:12 -0000
@@ -2865,6 +2865,7 @@ class DatabaseTransactionTestCase extend
    */
   protected function transactionOuterLayer($suffix, $rollback = FALSE) {
     $connection = Database::getConnection();
+    $depth = $connection->transactionDepth();
     $txn = db_transaction();
 
     // Insert a single row into the testing table.
@@ -2882,6 +2883,13 @@ class DatabaseTransactionTestCase extend
     $this->transactionInnerLayer($suffix, $rollback);
 
     $this->assertTrue($connection->inTransaction(), t('In transaction after calling nested transaction.'));
+
+    if ($rollback) {
+      // Roll back the transaction, if requested.
+      // This rollback should rollback to the last savepoint.
+      $txn->rollback();
+      $this->assertTrue(($connection->transactionDepth() == $depth), t('Transaction has rolled back to the last savepoint after calling rollback().'));
+    }
   }
 
   /**
@@ -2896,12 +2904,18 @@ class DatabaseTransactionTestCase extend
   protected function transactionInnerLayer($suffix, $rollback = FALSE) {
     $connection = Database::getConnection();
 
+    $this->assertTrue($connection->inTransaction(), t('In transaction in nested transaction.'));
+
+    $depth = $connection->transactionDepth();
     // Start a transaction. If we're being called from ->transactionOuterLayer,
     // then we're already in a transaction. Normally, that would make starting
     // a transaction here dangerous, but the database API handles this problem
     // for us by tracking the nesting and avoiding the danger.
     $txn = db_transaction();
 
+    $depth2 = $connection->transactionDepth();
+    $this->assertTrue($depth < $depth2, t('Transaction depth is has increased with new transaction.'));
+
     // Insert a single row into the testing table.
     db_insert('test')
       ->fields(array(
@@ -2914,9 +2928,9 @@ class DatabaseTransactionTestCase extend
 
     if ($rollback) {
       // Roll back the transaction, if requested.
-      // This rollback should propagate to the the outer transaction, if present.
+      // This rollback should rollback to the last savepoint.
       $txn->rollback();
-      $this->assertTrue($txn->willRollback(), t('Transaction is scheduled to roll back after calling rollback().'));
+      $this->assertTrue(($connection->transactionDepth() == $depth), t('Transaction has rolled back to the last savepoint after calling rollback().'));
     }
   }
 
