From b0a04e5e2088b77c20f2245a20b81af9492da20b Mon Sep 17 00:00:00 2001
From: Darren Oh <darren@oh.name>
Date: Fri, 12 Sep 2014 16:28:34 -0400
Subject: [PATCH] Issue #805858 by Steven Jones, Pancho, and Darren Oh: Fixed
 inconsistent results for number of rows affected by update
 queries.

---
 includes/database/pgsql/query.inc  |    5 +++-
 includes/database/query.inc        |   43 ++++++++++++++++++++++++++++++++
 includes/database/sqlite/query.inc |   47 ------------------------------------
 3 files changed, 47 insertions(+), 48 deletions(-)

diff --git a/includes/database/pgsql/query.inc b/includes/database/pgsql/query.inc
index 9902b16..73c23b0 100644
--- a/includes/database/pgsql/query.inc
+++ b/includes/database/pgsql/query.inc
@@ -154,6 +154,9 @@ class UpdateQuery_pgsql extends UpdateQuery {
     $blobs = array();
     $blob_count = 0;
 
+    // Unselect rows that don't have to be changed on update ('no-op rows').
+    $this->unselectNoOpRows();
+
     // Because we filter $fields the same way here and in __toString(), the
     // placeholders will all match up properly.
     $stmt = $this->connection->prepareQuery((string) $this);
@@ -203,7 +206,7 @@ class UpdateQuery_pgsql extends UpdateQuery {
 
     $options = $this->queryOptions;
     $options['already_prepared'] = TRUE;
-    $this->connection->query($stmt, $options);
+    $this->connection->query($stmt, array(), $options);
 
     return $stmt->rowCount();
   }
diff --git a/includes/database/query.inc b/includes/database/query.inc
index 8af91c2..05974fb 100644
--- a/includes/database/query.inc
+++ b/includes/database/query.inc
@@ -1150,6 +1150,8 @@ class UpdateQuery extends Query implements QueryConditionInterface {
    *   The number of rows affected by the update.
    */
   public function execute() {
+    // Unselect rows that don't have to be changed on update ('no-op rows').
+    $this->unselectNoOpRows();
 
     // Expressions take priority over literal fields, so we process those first
     // and remove any literal fields that conflict.
@@ -1178,6 +1180,47 @@ class UpdateQuery extends Query implements QueryConditionInterface {
   }
 
   /**
+   * Unselect rows that don't have to be changed on update ('no-op rows').
+   *
+   * PostgreSQL, SQLite and some versions of MySQL using the InnoDB engine
+   * count all rows matched by the query as affected rows, even if they will not
+   * be changed. To ensure consistency between all database systems, we work
+   * around that by rewriting the query ensuring that these rows aren't selected
+   * from the beginning.
+   *
+   * A query like this one:
+   *   UPDATE test SET col1 = 'newcol1', col2 = 'newcol2' WHERE tid = 1
+   * will become:
+   *   UPDATE test SET col1 = 'newcol1', col2 = 'newcol2' WHERE tid = 1 AND (col1 <> 'newcol1' OR col2 <> 'newcol2')
+   */
+  protected function unselectNoOpRows() {
+    // Get the fields used in the update query.
+    $fields = $this->expressionFields + $this->fields;
+
+    // Add the inverse of the fields to the condition.
+    $condition = new DatabaseCondition('OR');
+    foreach ($fields as $field => $data) {
+      if (is_array($data)) {
+        // The field is an expression.
+        $condition->where($field . ' <> ' . $data['expression']);
+        $condition->isNull($field);
+      }
+      elseif (!isset($data)) {
+        // The field will be set to NULL.
+        $condition->isNotNull($field);
+      }
+      else {
+        $condition->condition($field, $data, '<>');
+        $condition->isNull($field);
+      }
+    }
+    if (count($condition)) {
+      $condition->compile($this->connection, $this);
+      $this->condition->where((string) $condition, $condition->arguments());
+    }
+  }
+
+  /**
    * Implements PHP magic __toString method to convert the query to a string.
    *
    * @return string
diff --git a/includes/database/sqlite/query.inc b/includes/database/sqlite/query.inc
index 1c6289b..bfdeaae 100644
--- a/includes/database/sqlite/query.inc
+++ b/includes/database/sqlite/query.inc
@@ -51,53 +51,6 @@ class InsertQuery_sqlite extends InsertQuery {
 }
 
 /**
- * SQLite specific implementation of UpdateQuery.
- *
- * SQLite counts all the rows that match the conditions as modified, even if they
- * will not be affected by the query. We workaround this by ensuring that
- * we don't select those rows.
- *
- * A query like this one:
- *   UPDATE test SET col1 = 'newcol1', col2 = 'newcol2' WHERE tid = 1
- * will become:
- *   UPDATE test SET col1 = 'newcol1', col2 = 'newcol2' WHERE tid = 1 AND (col1 <> 'newcol1' OR col2 <> 'newcol2')
- */
-class UpdateQuery_sqlite extends UpdateQuery {
-  public function execute() {
-    if (!empty($this->queryOptions['sqlite_return_matched_rows'])) {
-      return parent::execute();
-    }
-
-    // Get the fields used in the update query.
-    $fields = $this->expressionFields + $this->fields;
-
-    // Add the inverse of the fields to the condition.
-    $condition = new DatabaseCondition('OR');
-    foreach ($fields as $field => $data) {
-      if (is_array($data)) {
-        // The field is an expression.
-        $condition->where($field . ' <> ' . $data['expression']);
-        $condition->isNull($field);
-      }
-      elseif (!isset($data)) {
-        // The field will be set to NULL.
-        $condition->isNotNull($field);
-      }
-      else {
-        $condition->condition($field, $data, '<>');
-        $condition->isNull($field);
-      }
-    }
-    if (count($condition)) {
-      $condition->compile($this->connection, $this);
-      $this->condition->where((string) $condition, $condition->arguments());
-    }
-    return parent::execute();
-  }
-
-}
-
-/**
  * SQLite specific implementation of DeleteQuery.
  *
  * When the WHERE is omitted from a DELETE statement and the table being deleted
-- 
1.7.5.4

