#961604: double-wrap subqueries from UPDATE and DELETE queries to workaround MySQL query planner oddities.

From: Damien Tournoud <damien@commerceguys.com>


---
 database/mysql/query.inc |   54 ++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 54 insertions(+), 0 deletions(-)

diff --git includes/database/mysql/query.inc includes/database/mysql/query.inc
index b1f248a..97f90f3 100644
--- includes/database/mysql/query.inc
+++ includes/database/mysql/query.inc
@@ -104,5 +104,59 @@ class TruncateQuery_mysql extends TruncateQuery {
 }
 
 /**
+ * MySQL-specific implementation of SelectQuery.
+ *
+ * MySQL doesn't properly support subqueries in DML statements like UPDATE or
+ * DELETE, because of issues in the correlated queries optimizer:
+ *  * In some cases, the optimizer code can execute the subquery for every row
+ *    of the main query, which is horribly inefficient.
+ *  * MySQL doesn't allow the base table of the UPDATE or DELETE query to be
+ *    referenced in any subquery.
+ *
+ * We force the engine to materialize (ie. generate a temporary table) for
+ * those subqueries by "double-wrapping" them:
+ *
+ * @code
+ *   UPDATE table SET col = 1 WHERE id = (SELECT id FROM table2);
+ * @endcode
+ *
+ * becomes:
+ *
+ * @code
+ *   UPDATE table SET col = 1 WHERE id = (SELECT * FROM (SELECT id FROM table2) alias));
+ * @endcode
+ */
+class SelectQuery_mysql extends SelectQuery {
+  function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder = NULL) {
+    if (empty($queryPlaceholder->preventDoubleWrap) && isset($queryPlaceholder) && ($queryPlaceholder instanceof UpdateQuery || $queryPlaceholder instanceof DeleteQuery)) {
+      // Disable double wrapping for subqueries of this query.
+      $queryPlaceholder->preventDoubleWrap = TRUE;
+
+      // Compile the sub-query using wrapping.
+      $this->doubleWrap = TRUE;
+      $this->wrapTableName = 'db_subquery_table_' . $queryPlaceholder->nextPlaceholder();
+      $output = parent::compile($connection, $queryPlaceholder);
+
+      // Reenable wrapping for the next subquery.
+      unset($queryPlaceholder->preventDoubleWrap);
+
+      return $output;
+    }
+    else {
+      return parent::compile($connection, $queryPlaceholder);
+    }
+  }
+
+  function __toString() {
+    if (!empty($this->doubleWrap)) {
+      return 'SELECT * FROM (' . parent::__toString() . ') ' . $this->wrapTableName;
+    }
+    else {
+      return parent::__toString();
+    }
+  }
+}
+
+/**
  * @} End of "ingroup database".
  */
