#715108: fix the semantics of MergeQuery on MySQL. From: Damien Tournoud --- database/mysql/query.inc | 120 +++------------------------------------------- database/query.inc | 68 ++++++++++++++------------ 2 files changed, 46 insertions(+), 142 deletions(-) diff --git includes/database/mysql/query.inc includes/database/mysql/query.inc index 70770f4..fb9a364 100644 --- includes/database/mysql/query.inc +++ includes/database/mysql/query.inc @@ -87,121 +87,19 @@ class InsertQuery_mysql extends InsertQuery { } } -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++; + return parent::__toString(); } - - $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]); - } - - // 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; } } diff --git includes/database/query.inc includes/database/query.inc index 1a9be01..c038102 100644 --- includes/database/query.inc +++ includes/database/query.inc @@ -810,50 +810,56 @@ class MergeQuery extends Query { $transaction = $this->connection->startTransaction(); // 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); } - $select = $select->countQuery(); - $sql = (string) $select; + // Using SELECT FOR UPDATE syntax will lock the rows we want to attempt to update. + $sql = ((string) $select) . ' FOR UPDATE'; $arguments = $select->getArguments(); - $num_existing = $this->connection->query($sql, $arguments)->fetchField(); - - if ($num_existing) { - // If there is already an existing record, run an update query. - - if ($this->updateFields) { - $update_fields = $this->updateFields; - } - 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]); + try { + // If there are already existing records, run an update query. + if ($this->connection->query($sql, $arguments)->fetchField()) { + if ($this->updateFields) { + $update_fields = $this->updateFields; } - } - if ($update_fields || $this->expressionFields) { - // Only run the update if there are no 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); + 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]); + } } - foreach ($this->expressionFields as $field => $expression) { - $update->expression($field, $expression['expression'], $expression['arguments']); + if ($update_fields || $this->expressionFields) { + // Only run the update if there are no 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); + } + foreach ($this->expressionFields as $field => $expression) { + $update->expression($field, $expression['expression'], $expression['arguments']); + } + $update->execute(); + return MergeQuery::STATUS_UPDATE; } - $update->execute(); - 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; } } - 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) { + $transaction->rollback(); + return FALSE; } - // Transaction commits here where $transaction looses scope. }