Index: includes/database/query.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database/query.inc,v retrieving revision 1.51 diff -u -p -r1.51 query.inc --- includes/database/query.inc 2 Jun 2010 19:39:17 -0000 1.51 +++ includes/database/query.inc 17 Jun 2010 00:08:18 -0000 @@ -801,29 +801,45 @@ class MergeQuery extends Query { // In the degenerate case of this query type, we have to run multiple // queries as there is no universal single-query mechanism that will work. - // Our degenerate case is not designed for performance efficiency but - // for comprehensibility. Any practical database driver will override - // this method with database-specific logic, so this function serves only - // as a fallback to aid developers of new drivers. // Wrap multiple queries in a transaction, if the database supports it. $transaction = $this->connection->startTransaction(); - // Manually check if the record already exists. - $select = $this->connection->select($this->table); - foreach ($this->keyFields as $field => $value) { - $select->condition($field, $value); - } - - $select = $select->countQuery(); - $sql = (string) $select; - $arguments = $select->getArguments(); - $num_existing = $this->connection->query($sql, $arguments)->fetchField(); - + try { + // Manually check if the record already exists. + // We build a 'SELECT 1 FROM table WHERE conditions FOR UPDATE' query, + // that will lock the rows that matches our set of conditions as well as + // return the information that there are such rows. + $select = $this->connection->select($this->table); + $select->addExpression('1'); + foreach ($this->keyFields as $field => $value) { + $select->condition($field, $value); + } - if ($num_existing) { - // If there is already an existing record, run an update query. + // Using SELECT FOR UPDATE syntax will lock the rows we want to attempt to update. + $sql = ((string) $select) . ' FOR UPDATE'; + $arguments = $select->getArguments(); + + // If there are no existing records, run an insert query. + if (!$this->connection->query($sql, $arguments)->fetchField()) { + // If there is no existing record, run an insert query. + $insert_fields = $this->insertFields + $this->keyFields; + try { + $this->connection->insert($this->table, $this->queryOptions)->fields($insert_fields)->execute(); + return MergeQuery::STATUS_INSERT; + } + catch (Exception $e) { + // The insert query failed, maybe it's because a racing insert query + // beat us in inserting the same row. Retry the select query, if it + // returns a row, ignore the error and continue with the update + // query below. + if (!$this->connection->query($sql, $arguments)->fetchField()) { + throw $e; + } + } + } + // Proceed with an update query if a row was found. if ($this->updateFields) { $update_fields = $this->updateFields; } @@ -835,7 +851,7 @@ class MergeQuery extends Query { } } if ($update_fields || $this->expressionFields) { - // Only run the update if there are no fields or expressions to update. + // Only run the update if there are fields or expressions to update. $update = $this->connection->update($this->table, $this->queryOptions)->fields($update_fields); foreach ($this->keyFields as $field => $value) { $update->condition($field, $value); @@ -847,13 +863,12 @@ class MergeQuery extends Query { return MergeQuery::STATUS_UPDATE; } } - else { - // If there is no existing record, run an insert query. - $insert_fields = $this->insertFields + $this->keyFields; - $this->connection->insert($this->table, $this->queryOptions)->fields($insert_fields)->execute(); - return MergeQuery::STATUS_INSERT; + catch (Exception $e) { + // Something really wrong happened here, bubble up the exception to the + // caller. + $transaction->rollback(); + throw $e; } - // Transaction commits here where $transaction looses scope. } Index: includes/database/mysql/query.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database/mysql/query.inc,v retrieving revision 1.17 diff -u -p -r1.17 query.inc --- includes/database/mysql/query.inc 15 May 2010 07:04:21 -0000 1.17 +++ includes/database/mysql/query.inc 17 Jun 2010 00:08:19 -0000 @@ -87,121 +87,19 @@ class InsertQuery_mysql extends InsertQu } } -class MergeQuery_mysql extends MergeQuery { - - public function execute() { - - // A merge query without any key field is invalid. - if (count($this->keyFields) == 0) { - throw new InvalidMergeQueryException("You need to specify key fields before executing a merge query"); - } - - // Set defaults. - if ($this->updateFields) { - $update_fields = $this->updateFields; - } - else { - // When update fields are derived from insert fields, we don't need - // placeholders since we can tell MySQL to reuse insert supplied - // values using the VALUES(col_name) function. - $update_fields = array(); - } - - $insert_fields = $this->insertFields + $this->keyFields; - - $max_placeholder = 0; - $values = array(); - // We assume that the order here is the same as in __toString(). If that's - // not the case, then we have serious problems. - foreach ($insert_fields as $value) { - $values[':db_insert_placeholder_' . $max_placeholder++] = $value; - } - - // Expressions take priority over literal fields, so we process those first - // and remove any literal fields that conflict. - foreach ($this->expressionFields as $field => $data) { - if (!empty($data['arguments'])) { - $values += $data['arguments']; - } - unset($update_fields[$field]); - } - - // Because we filter $fields the same way here and in __toString(), the - // placeholders will all match up properly. - $max_placeholder = 0; - foreach ($update_fields as $field => $value) { - $values[':db_update_placeholder_' . ($max_placeholder++)] = $value; - } - - - // MySQL's INSERT ... ON DUPLICATE KEY UPDATE queries return 1 - // (MergeQuery::STATUS_INSERT) for an INSERT operation or 2 - // (MergeQuery::STATUS_UPDATE) for an UPDATE operation. - // - // @link http ://dev.mysql.com/doc/refman/5.0/en/mysql-affected-rows.html - $this->queryOptions['return'] = Database::RETURN_AFFECTED; - return $this->connection->query((string) $this, $values, $this->queryOptions); - } - - +class TruncateQuery_mysql extends TruncateQuery { public function __toString() { - // Create a comments string to prepend to the query. - $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; - - // Set defaults. - if ($this->updateFields) { - $update_fields = $this->updateFields; + // TRUNCATE is actually a DDL statement on MySQL, and DDL statements are + // not transactional, and result in an implicit COMMIT. When we are in a + // transaction, fallback to the slower, but transactional, DELETE. + if ($this->connection->inTransaction()) { + // Create a comments string to prepend to the query. + $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}'; } else { - $update_fields = $this->insertFields; - // If there are no exclude fields, this is a no-op. - foreach ($this->excludeFields as $exclude_field) { - unset($update_fields[$exclude_field]); - } - } - - // If the merge query has no fields to update, add the first key as an - // update field so the query will not fail if a duplicate key is found. - if (!$update_fields && !$this->expressionFields) { - $update_fields = array_slice($this->keyFields, 0, 1, TRUE); - } - - $insert_fields = $this->insertFields + $this->keyFields; - - $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', array_keys($insert_fields)) . ') VALUES '; - - $max_placeholder = 0; - $values = array(); - // We don't need the $field, but this is a convenient way to count. - foreach ($insert_fields as $field) { - $values[] = ':db_insert_placeholder_' . $max_placeholder++; - } - - $query .= '(' . implode(', ', $values) . ') ON DUPLICATE KEY UPDATE '; - - // Expressions take priority over literal fields, so we process those first - // and remove any literal fields that conflict. - $max_placeholder = 0; - $update = array(); - foreach ($this->expressionFields as $field => $data) { - $update[] = $field . '=' . $data['expression']; - unset($update_fields[$field]); + return parent::__toString(); } - - // Build update fields clauses based on caller supplied list, or derived - // from insert supplied values using the VALUES(col_name) function. - foreach ($update_fields as $field => $value) { - if ($this->updateFields) { - $update[] = ($field . '=:db_update_placeholder_' . $max_placeholder++); - } - else { - $update[] = ($field . '=VALUES(' . $field . ')'); - } - } - - $query .= implode(', ', $update); - - return $query; } }