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..a1a2937
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Database\Driver\mysql\Upsert.
+ */
+
+namespace Drupal\Core\Database\Driver\mysql;
+
+use Drupal\Core\Database\Query\Upsert as QueryUpsert;
+
+class Upsert extends QueryUpsert {
+
+  /**
+   * {@inheritdoc}}
+   */
+  public function execute() {
+    if (!$this->preExecute()) {
+      return NULL;
+    }
+
+    // If we're selecting from a SelectQuery, finish building the query and
+    // pass it back, as any remaining options are irrelevant.
+    $max_placeholder = 0;
+    $values = array();
+    foreach ($this->insertValues as $insert_values) {
+      foreach ($insert_values as $value) {
+        $values[':db_insert_placeholder_' . $max_placeholder++] = $value;
+      }
+    }
+
+    $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);
+
+    $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
+
+    $max_placeholder = 0;
+    $values = array();
+    if ($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);
+
+    $update = [];
+    foreach ($insert_fields as $field) {
+      $update[] = "$field = VALUES($field)";
+    }
+
+    $query .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $update);
+
+    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..d322130
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/Query/Upsert.php
@@ -0,0 +1,174 @@
+<?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.
+ *
+ * This class should only be used with a table with a single unique index.
+ * Often, this will be the primary key. On such a table this class works like
+ * Insert except the rows will be set to the desired values even if the key
+ * existed before.
+ */
+class Upsert extends Query {
+
+  /**
+   * 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 array
+   */
+  protected $insertValues = array();
+
+  /**
+   * 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;
+  }
+
+  /**
+   * 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 fields(array $fields, array $values = array()) {
+    if (!$this->insertFields) {
+      if (!$values) {
+        if (!is_numeric(array_keys($fields)[0])) {
+          $values = array_values($fields);
+          $fields = array_keys($fields);
+        }
+      }
+      $this->insertFields = $fields;
+      if (!empty($values)) {
+        $this->insertValues[] = $values;
+      }
+    }
+    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 values(array $values) {
+    if (is_numeric(key($values))) {
+      $this->insertValues[] = $values;
+    }
+    elseif ($this->insertFields) {
+      // 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
+   *   TRUE if the validation was successful, FALSE if not.
+   *
+   * @throws \Drupal\Core\Database\Query\FieldsOverlapException
+   * @throws \Drupal\Core\Database\Query\NoFieldsException
+   */
+  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.');
+    }
+
+    // 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;
+  }
+
+  /**
+   * {@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..298aa80
--- /dev/null
+++ b/core/modules/system/src/Tests/Database/UpsertTest.php
@@ -0,0 +1,72 @@
+<?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.');
+  }
+
+}
