diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php
index 33d0337..b2e28d8 100644
--- a/core/lib/Drupal/Core/Database/Connection.php
+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -786,6 +786,21 @@ public function merge($table, array $options = array()) {
     return new $class($this, $table, $options);
   }
 
+  /**
+   * Prepares and returns an UPSERT query object.
+   *
+   * @param array $options
+   *   An array of options on the query.
+   *
+   * @return \Drupal\Core\Database\Query\Upsert
+   *   A new Upsert query object.
+   *
+   * @see \Drupal\Core\Database\Query\Upsert
+   */
+  public function upsert($table, array $options = array()) {
+    $class = $this->getDriverClass('Upsert');
+    return new $class($this, $table, $options);
+  }
 
   /**
    * Prepares and returns an UPDATE query object.
diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php b/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
new file mode 100644
index 0000000..c650bfb
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
@@ -0,0 +1,152 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Database\Driver\mysql\Upsert.
+ */
+
+namespace Drupal\Core\Database\Driver\mysql;
+
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\Database\Query\Upsert as QueryUpsert;
+
+class Upsert extends QueryUpsert {
+
+  public function updateValues(array $fields) {
+    foreach ($fields as $field) {
+      $this->expression($field, 'VALUES(' . $this->connection->escapeField($field) . ')');
+    }
+  }
+
+  /**
+   * {@inheritdoc}}
+   */
+  public function execute() {
+
+    if (!$this->preExecute()) {
+      return NULL;
+    }
+    $max_placeholder = 0;
+
+    // If we're selecting from a SelectQuery, finish building the query and
+    // pass it back, as any remaining options are irrelevant.
+    if (empty($this->fromQuery)) {
+      $values = array();
+      foreach ($this->insertValues as $insert_values) {
+        foreach ($insert_values as $value) {
+          $values[':db_insert_placeholder_' . $max_placeholder++] = $value;
+        }
+      }
+    }
+    else {
+      $values = $this->fromQuery->getArguments();
+    }
+    if ($this->needsUpdate) {
+      $fields = $this->updateFields;
+      foreach ($this->expressionFields as $field => $data) {
+        if (!empty($data['arguments'])) {
+          $values += $data['arguments'];
+        }
+        if ($data['expression'] instanceof SelectInterface) {
+          $data['expression']->compile($this->connection, $this);
+          $values += $data['expression']->arguments();
+        }
+        unset($fields[$field]);
+      }
+
+      // Because we filter $fields the same way here and in __toString(), the
+      // placeholders will all match up properly.
+      foreach ($fields as $value) {
+        $values[':db_update_placeholder_' . $max_placeholder++] = $value;
+      }
+
+      if (count($this->condition)) {
+        $this->condition->compile($this->connection, $this);
+        $values = array_merge($values, $this->condition->arguments());
+      }
+    }
+
+    $last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions);
+
+    // Re-initialize the values array so that we can re-use this query.
+    $this->insertValues = array();
+
+    return $last_insert_id;
+  }
+
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    // Default fields are always placed first for consistency.
+    $insert_fields = array_merge($this->defaultFields, $this->insertFields);
+    $max_placeholder = 0;
+
+    // If we're selecting from a SelectQuery, finish building the query and
+    // pass it back, as any remaining options are irrelevant.
+    if (!empty($this->fromQuery)) {
+      $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
+      $query = $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
+    }
+    else {
+      $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
+
+      $values = array();
+      if (count($this->insertValues)) {
+        foreach ($this->insertValues as $insert_values) {
+          $placeholders = array();
+
+          // Default fields aren't really placeholders, but this is the most convenient
+          // way to handle them.
+          $placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
+
+          $new_placeholder = $max_placeholder + count($insert_values);
+          for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
+            $placeholders[] = ':db_insert_placeholder_' . $i;
+          }
+          $max_placeholder = $new_placeholder;
+          $values[] = '(' . implode(', ', $placeholders) . ')';
+        }
+      }
+      else {
+        // If there are no values, then this is a default-only query. We still need to handle that.
+        $placeholders = array_fill(0, count($this->defaultFields), 'default');
+        $values[] = '(' . implode(', ', $placeholders) . ')';
+      }
+
+      $query .= implode(', ', $values);
+    }
+
+    if ($this->needsUpdate) {
+      // Expressions take priority over literal fields, so we process those first
+      // and remove any literal fields that conflict.
+      $fields = $this->updateFields;
+      $update_fields = array();
+      foreach ($this->expressionFields as $field => $data) {
+        if ($data['expression'] instanceof SelectInterface) {
+          // Compile and cast expression subquery to a string.
+          $data['expression']->compile($this->connection, $this);
+          $data['expression'] = ' (' . $data['expression'] . ')';
+        }
+        $update_fields[] = $this->connection->escapeField($field) . '=' . $data['expression'];
+        unset($fields[$field]);
+      }
+
+      foreach ($fields as $field => $value) {
+        $update_fields[] = $this->connection->escapeField($field) . '=:db_update_placeholder_' . ($max_placeholder++);
+      }
+
+      $query .= 'ON DUPLICATE KEY UPDATE  ' . implode(', ', $update_fields);
+
+      if (count($this->condition)) {
+        $this->condition->compile($this->connection, $this);
+        // There is an implicit string cast on $this->condition.
+        $query .= "\nWHERE " . $this->condition;
+      }
+
+    }
+
+    return $query;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php
new file mode 100644
index 0000000..f70956e
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Database\Driver\pgsql\Upsert.
+ */
+
+namespace Drupal\Core\Database\Driver\pgsql;
+
+use Drupal\Core\Database\Query\Upsert as QueryUpsert;
+
+class Upsert extends QueryUpsert { }
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php
new file mode 100644
index 0000000..bdb3dbf
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Database\Driver\sqlite\Upsert.
+ */
+
+namespace Drupal\Core\Database\Driver\sqlite;
+
+use Drupal\Core\Database\Query\Upsert as QueryUpsert;
+
+class Upsert extends QueryUpsert { }
diff --git a/core/lib/Drupal/Core/Database/Query/Upsert.php b/core/lib/Drupal/Core/Database/Query/Upsert.php
new file mode 100644
index 0000000..b1858e9
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/Query/Upsert.php
@@ -0,0 +1,360 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Database\Query\Upsert.
+ */
+
+namespace Drupal\Core\Database\Query;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\Database;
+
+/**
+ * General class for an abstracted "Upsert" (UPDATE or INSERT) query operation.
+ *
+ * @todo See if the STATUS_INSERT and STATUS_UPDATE constants are still needed
+ * or usable since we will most likely have multiple inserts or updates in the
+ * same statement.
+ */
+class Upsert extends Query {
+
+  use QueryConditionTrait;
+
+  /**
+   * Returned by execute() if an INSERT query has been executed.
+   */
+  const STATUS_INSERT = 1;
+
+  /**
+   * Returned by execute() if an UPDATE query has been executed.
+   */
+  const STATUS_UPDATE = 2;
+
+  /**
+   * The table to be used for INSERT and UPDATE.
+   *
+   * @var string
+   */
+  protected $table;
+
+  /**
+   * An array of fields on which to insert.
+   *
+   * @var array
+   */
+  protected $insertFields = array();
+
+  /**
+   * An array of fields which should be set to their database-defined defaults.
+   *
+   * Used on INSERT.
+   *
+   * @var array
+   */
+  protected $defaultFields = array();
+
+  /**
+   * An array of values to be inserted.
+   *
+   * @var string
+   */
+  protected $insertValues = array();
+
+  /**
+   * An array of fields that will be updated.
+   *
+   * @var array
+   */
+  protected $updateFields = array();
+
+  /**
+   * @var array
+   */
+  protected $updateValues = array();
+
+  /**
+   * Array of fields to update to an expression in case of a duplicate record.
+   *
+   * This variable is a nested array in the following format:
+   * @code
+   * <some field> => array(
+   *  'condition' => <condition to execute, as a string>,
+   *  'arguments' => <array of arguments for condition, or NULL for none>,
+   * );
+   * @endcode
+   *
+   * @var array
+   */
+  protected $expressionFields = array();
+
+  /**
+   * Flag indicating whether an UPDATE is necessary.
+   *
+   * @var bool
+   */
+  protected $needsUpdate = FALSE;
+
+  /**
+   * A SelectQuery object to fetch the rows that should be inserted.
+   *
+   * @var \Drupal\Core\Database\Query\SelectInterface
+   */
+  protected $fromQuery;
+
+  /**
+   * Constructs an Upsert object.
+   *
+   * @param \Drupal\Core\Database\Connection $connection
+   *   A Connection object.
+   * @param string $table
+   *   Name of the table to associate with this query.
+   * @param array $options
+   *   Array of database options.
+   */
+  public function __construct(Connection $connection, $table, array $options = array()) {
+    $options['return'] = Database::RETURN_AFFECTED;
+    parent::__construct($connection, $options);
+    $this->table = $table;
+    $this->conditionTable = $table;
+    $this->condition = new Condition('AND');
+  }
+
+  /**
+   * Adds a set of field->value pairs to be updated.
+   *
+   * @param array $fields
+   *   An associative array of fields to write into the database. The array keys
+   *   are the field names and the values are the values to which to set them.
+   *
+   * @return \Drupal\Core\Database\Query\Upsert
+   *   The called object.
+   */
+  public function updateFields(array $fields) {
+    $this->updateFields = $fields;
+    $this->needsUpdate = TRUE;
+    return $this;
+  }
+
+  /**
+   * A list of fields to update to the same value as they would be on INSERT.
+   *
+   * @param array $fields
+   *   An associative array of fields to write into the database. The array keys
+   *   are the field names and the values are the values to which to set them.
+   *
+   * @return $this
+   *   The called object.
+   */
+  public function updateValues(array $fields) {
+    $this->updateValues = $fields;
+    $this->needsUpdate = TRUE;
+    return $this;
+  }
+
+  /**
+   * Specifies fields to be updated as an expression.
+   *
+   * Expression fields are cases such as counter = counter + 1. This method
+   * takes precedence over Upsert::updateFields() and it's wrappers,
+   * Upsert::key() and Upsert::fields().
+   *
+   * @param $field
+   *   The field to set.
+   * @param $expression
+   *   The field will be set to the value of this expression. This parameter
+   *   may include named placeholders.
+   * @param array $arguments
+   *   If specified, this is an array of key/value pairs for named placeholders
+   *   corresponding to the expression.
+   *
+   * @return \Drupal\Core\Database\Query\Upsert
+   *   The called object.
+   */
+  public function expression($field, $expression, array $arguments = NULL) {
+    $this->expressionFields[$field] = array(
+      'expression' => $expression,
+      'arguments' => $arguments,
+    );
+    $this->needsUpdate = TRUE;
+    return $this;
+  }
+
+  /**
+   * Adds a set of field->value pairs to be inserted.
+   *
+   * @param $fields
+   *   An array of fields on which to insert. This array may be indexed or
+   *   associative. If indexed, the array is taken to be the list of fields.
+   *   If associative, the keys of the array are taken to be the fields and
+   *   the values are taken to be corresponding values to insert. If a
+   *   $values argument is provided, $fields must be indexed.
+   * @param $values
+   *   An array of fields to insert into the database. The values must be
+   *   specified in the same order as the $fields array.
+   *
+   * @return \Drupal\Core\Database\Query\Upsert
+   *   The called object.
+   */
+  public function insertFields(array $fields, array $values = array()) {
+    if (empty($this->insertFields)) {
+      if (empty($values)) {
+        if (!is_numeric(key($fields))) {
+          $values = array_values($fields);
+          $fields = array_keys($fields);
+        }
+      }
+      $this->insertFields = $fields;
+      if (!empty($values)) {
+        $this->insertValues[] = $values;
+      }
+    }
+    return $this;
+  }
+
+  /**
+   * Specifies fields for which the database-defaults should be used.
+   *
+   * If you want to force a given field to use the database-defined default,
+   * not NULL or undefined, use this method to instruct the database to use
+   * default values explicitly. In most cases this will not be necessary
+   * unless you are inserting a row that is all default values, as you cannot
+   * specify no values in an INSERT query.
+   *
+   * Specifying a field both in fields() and in useDefaults() is an error
+   * and will not execute.
+   *
+   * @param $fields
+   *   An array of values for which to use the default values
+   *   specified in the table definition.
+   *
+   * @return \Drupal\Core\Database\Query\Upsert
+   *   The called object.
+   */
+  public function useDefaults(array $fields) {
+    $this->defaultFields = $fields;
+    return $this;
+  }
+
+  /**
+   * Sets common field-value pairs in the INSERT and UPDATE query parts.
+   *
+   * This method should only be called once. It may be called either
+   * with a single associative array or two indexed arrays. If called
+   * with an associative array, the keys are taken to be the fields
+   * and the values are taken to be the corresponding values to set.
+   * If called with two arrays, the first array is taken as the fields
+   * and the second array is taken as the corresponding values.
+   *
+   * @param $fields
+   *   An array of fields to insert, or an associative array of fields and
+   *   values. The keys of the array are taken to be the fields and the values
+   *   are taken to be corresponding values to insert.
+   * @param $values
+   *   An array of values to set into the database. The values must be
+   *   specified in the same order as the $fields array.
+   *
+   * @return \Drupal\Core\Database\Query\Upsert
+   *   The called object.
+   */
+  public function fields(array $fields, array $values = array()) {
+    $this->insertFields($fields, $values);
+    if ($values) {
+      $this->updateFields(array_combine($fields, $values));
+    }
+    elseif (!is_numeric(array_keys($fields)[0])) {
+      $this->updateFields($fields);
+    }
+    else {
+      $this->updateValues($fields);
+    }
+    return $this;
+  }
+
+  /**
+   * Adds another set of values to the query to be inserted.
+   *
+   * If $values is a numeric-keyed array, it will be assumed to be in the same
+   * order as the original fields() call. If it is associative, it may be
+   * in any order as long as the keys of the array match the names of the
+   * fields.
+   *
+   * @param array $values
+   *   An array of values to add to the query.
+   *
+   * @return $this
+   *   The called object.
+   */
+  public function insertValues(array $values) {
+    if (is_numeric(key($values))) {
+      $this->insertValues[] = $values;
+    }
+    else {
+      // Reorder the submitted values to match the fields array.
+      foreach ($this->insertFields as $key) {
+        $insert_values[$key] = $values[$key];
+      }
+      // For consistency, the values array is always numerically indexed.
+      $this->insertValues[] = array_values($insert_values);
+    }
+    return $this;
+  }
+
+  /**
+   * Preprocesses and validates the query.
+   *
+   * @return bool
+   *   TRUE if the validation was successful, FALSE if not.
+   *
+   * @throws \Drupal\Core\Database\Query\FieldsOverlapException
+   *   If an identical field and default value are specified.
+   *
+   * @throws \Drupal\Core\Database\Query\NoFieldsException
+   *   If there are no fields to insert.
+   */
+  public function preExecute() {
+    // Confirm that the user did not try to specify an identical
+    // field and default field.
+    if (array_intersect($this->insertFields, $this->defaultFields)) {
+      throw new FieldsOverlapException('You may not specify the same field to have a value and a schema-default value.');
+    }
+
+    if (!empty($this->fromQuery)) {
+      // We have to assume that the used aliases match the insert fields.
+      // Regular fields are added to the query before expressions, maintain the
+      // same order for the insert fields.
+      // This behavior can be overridden by calling fields() manually as only the
+      // first call to fields() does have an effect.
+      $this->insertFields(array_merge(array_keys($this->fromQuery->getFields()), array_keys($this->fromQuery->getExpressions())));
+    }
+    else {
+      // Don't execute query without fields.
+      if (count($this->insertFields) + count($this->defaultFields) == 0) {
+        throw new NoFieldsException('There are no fields available to insert with.');
+      }
+    }
+
+    // If no values have been added, silently ignore this query. This can happen
+    // if values are added conditionally, so we don't want to throw an
+    // exception.
+    return isset($this->insertValues[0]) || $this->insertFields || $this->fromQuery;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+
+  }
+
+  /**
+   * Implements PHP magic __toString method to convert the query to a string.
+   *
+   * In the degenerate case, there is no string-able query as this operation
+   * is potentially two queries.
+   *
+   * @return string
+   *   The prepared query statement.
+   */
+  public function __toString() { }
+
+}
diff --git a/core/modules/system/src/Tests/Database/UpsertTest.php b/core/modules/system/src/Tests/Database/UpsertTest.php
new file mode 100644
index 0000000..2c036c5
--- /dev/null
+++ b/core/modules/system/src/Tests/Database/UpsertTest.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Database\UpsertTest.
+ */
+
+namespace Drupal\system\Tests\Database;
+
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\Query\Upsert;
+
+/**
+ * Tests the Upsert query builder.
+ *
+ * @group Database
+ */
+class UpsertTest extends DatabaseTestBase {
+
+  /**
+   * Confirms that we can merge-insert a record successfully.
+   */
+  public function testMergeInsert() {
+    $connection = Database::getConnection();
+    $num_records_before = $connection->query('SELECT COUNT(*) FROM {test_people}')->fetchField();
+
+    $result = $connection->upsert('test_people')
+      ->fields(array(
+        'job' => 'Presenter',
+        'age' => 31,
+        'name' => 'Tiffany',
+      ))
+      ->execute();
+
+    $this->assertEqual($result, Upsert::STATUS_INSERT, 'Insert status returned.');
+
+    $num_records_after = $connection->query('SELECT COUNT(*) FROM {test_people}')->fetchField();
+    $this->assertEqual($num_records_before + 1, $num_records_after, 'Merge inserted properly.');
+
+    $person = $connection->query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Presenter'))->fetch();
+    $this->assertEqual($person->name, 'Tiffany', 'Name set correctly.');
+    $this->assertEqual($person->age, 31, 'Age set correctly.');
+    $this->assertEqual($person->job, 'Presenter', 'Job set correctly.');
+  }
+
+  /**
+   * Confirms that we can merge-update a record successfully.
+   */
+  public function testMergeUpdate() {
+    $connection = Database::getConnection();
+    $num_records_before = $connection->query('SELECT COUNT(*) FROM {test_people}')->fetchField();
+
+    $result = $connection->upsert('test_people')
+      ->fields(array(
+        'job' => 'Speaker',
+        'age' => 31,
+        'name' => 'Tiffany',
+      ))
+      ->execute();
+
+    $this->assertEqual($result, Upsert::STATUS_UPDATE, 'Update status returned.');
+
+    $num_records_after = $connection->query('SELECT COUNT(*) FROM {test_people}')->fetchField();
+    $this->assertEqual($num_records_before, $num_records_after, 'Merge updated properly.');
+
+    $person = $connection->query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Speaker'))->fetch();
+    $this->assertEqual($person->name, 'Tiffany', 'Name set correctly.');
+    $this->assertEqual($person->age, 31, 'Age set correctly.');
+    $this->assertEqual($person->job, 'Speaker', 'Job set correctly.');
+  }
+
+}
