diff --git a/core/includes/entity.inc b/core/includes/entity.inc
index cc539dc..3025433 100644
--- a/core/includes/entity.inc
+++ b/core/includes/entity.inc
@@ -57,16 +57,22 @@ function entity_get_bundles($entity_type = NULL) {
  *   NULL.
  */
 function entity_invoke_bundle_hook($hook, $entity_type, $bundle, $bundle_new = NULL) {
-  \Drupal::entityManager()->clearCachedBundles();
+  $entity_manager = \Drupal::entityManager();
+
+  $entity_manager->clearCachedBundles();
 
   // Notify the entity storage.
   $method = 'onBundle' . ucfirst($hook);
-  $storage = \Drupal::entityManager()->getStorage($entity_type);
+  $storage = $entity_manager->getStorage($entity_type);
   if (method_exists($storage, $method)) {
     $storage->$method($bundle, $bundle_new);
   }
   // Invoke hook_entity_bundle_*() hooks.
   \Drupal::moduleHandler()->invokeAll('entity_bundle_' . $hook, array($entity_type, $bundle, $bundle_new));
+  // Clear the cached field definitions (not needed in case of 'create').
+  if ($hook == 'rename' || $hook == 'delete') {
+    $entity_manager->clearCachedFieldDefinitions();
+  }
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
index b050ba0..e7cfa1f 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
@@ -10,16 +10,15 @@
 use Drupal\Component\Utility\String;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Database\Database;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\Core\Entity\Query\QueryInterface;
 use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler;
 use Drupal\Core\Entity\Sql\DefaultTableMapping;
 use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\field\Entity\FieldConfig;
-use Drupal\field\FieldConfigInterface;
-use Drupal\field\FieldConfigUpdateForbiddenException;
-use Drupal\field\FieldInstanceConfigInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -227,7 +226,7 @@ public function getSchema() {
    */
   protected function schemaHandler() {
     if (!isset($this->schemaHandler)) {
-      $this->schemaHandler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this);
+      $this->schemaHandler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this, $this->database);
     }
     return $this->schemaHandler;
   }
@@ -237,14 +236,14 @@ protected function schemaHandler() {
    */
   public function getTableMapping() {
     if (!isset($this->tableMapping)) {
+      $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
+      $base_field_definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
+      $table_mapping = new DefaultTableMapping($storage_definitions, $base_field_definitions);
+      $this->tableMapping = $table_mapping;
 
-      $definitions = array_filter($this->getFieldStorageDefinitions(), function (FieldStorageDefinitionInterface $definition) {
-        // @todo Remove the check for FieldDefinitionInterface::isMultiple() when
-        //   multiple-value base fields are supported in
-        //   https://drupal.org/node/2248977.
-        return !$definition->hasCustomStorage() && !$definition->isMultiple();
+      $definitions = array_filter($storage_definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
+        return $table_mapping->allowsSharedTableStorage($definition);
       });
-      $this->tableMapping = new DefaultTableMapping($definitions);
 
       $key_fields = array_values(array_filter(array($this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey)));
       $all_fields = array_keys($definitions);
@@ -324,7 +323,7 @@ public function getTableMapping() {
         $this->tableMapping->setFieldNames($this->revisionTable, $revision_base_fields);
 
         $revision_data_key_fields = array($this->idKey, $this->revisionKey, $this->langcodeKey);
-        $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields);
+        $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, array($this->langcodeKey));
         $this->tableMapping
           ->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields))
           // Add the denormalized 'default_langcode' field to the mapping. Its
@@ -332,6 +331,25 @@ public function getTableMapping() {
           // "revision_table.langcode = data_table.langcode".
           ->setExtraColumns($this->revisionDataTable, array('default_langcode'));
       }
+
+      // Add dedicated tables.
+      $definitions = array_filter($storage_definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
+        return $table_mapping->requiresDedicatedTableStorage($definition);
+      });
+      $extra_columns = array(
+        'bundle',
+        'deleted',
+        'entity_id',
+        'revision_id',
+        'langcode',
+        'delta',
+      );
+      foreach ($definitions as $field_name => $definition) {
+        foreach (array($table_mapping->getDedicatedDataTableName($definition), $table_mapping->getDedicatedRevisionTableName($definition)) as $table_name) {
+          $table_mapping->setFieldNames($table_name, array($field_name));
+          $table_mapping->setExtraColumns($table_name, $extra_columns);
+        }
+      }
     }
 
     return $this->tableMapping;
@@ -439,7 +457,7 @@ protected function attachPropertyData(array &$entities) {
       $table_mapping = $this->getTableMapping();
       $translations = array();
       if ($this->revisionDataTable) {
-        $data_fields = array_diff_key($table_mapping->getFieldNames($this->revisionDataTable), $table_mapping->getFieldNames($this->baseTable));
+        $data_fields = array_diff($table_mapping->getFieldNames($this->revisionDataTable), $table_mapping->getFieldNames($this->baseTable));
       }
       else {
         $data_fields = $table_mapping->getFieldNames($this->dataTable);
@@ -954,21 +972,23 @@ protected function doLoadFieldItems($entities, $age) {
     }
 
     // Collect impacted fields.
-    $fields = array();
+    $storage_definitions = array();
     $definitions = array();
+    $table_mapping = $this->getTableMapping();
     foreach ($bundles as $bundle => $v) {
       $definitions[$bundle] = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle);
-      foreach ($definitions[$bundle] as $field_name => $instance) {
-        if ($instance instanceof FieldInstanceConfigInterface) {
-          $fields[$field_name] = $instance->getFieldStorageDefinition();
+      foreach ($definitions[$bundle] as $field_name => $field_definition) {
+        $storage_definition = $field_definition->getFieldStorageDefinition();
+        if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+          $storage_definitions[$field_name] = $storage_definition;
         }
       }
     }
 
     // Load field data.
     $langcodes = array_keys(language_list(LanguageInterface::STATE_ALL));
-    foreach ($fields as $field_name => $field) {
-      $table = $load_current ? static::_fieldTableName($field) : static::_fieldRevisionTableName($field);
+    foreach ($storage_definitions as $field_name => $storage_definition) {
+      $table = $load_current ? $table_mapping->getDedicatedDataTableName($storage_definition) : $table_mapping->getDedicatedRevisionTableName($storage_definition);
 
       // Ensure that only values having valid languages are retrieved. Since we
       // are loading values for multiple entities, we cannot limit the query to
@@ -992,12 +1012,12 @@ protected function doLoadFieldItems($entities, $age) {
             $delta_count[$row->entity_id][$row->langcode] = 0;
           }
 
-          if ($field->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->langcode] < $field->getCardinality()) {
+          if ($storage_definition->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->langcode] < $storage_definition->getCardinality()) {
             $item = array();
             // For each column declared by the field, populate the item from the
             // prefixed database column.
-            foreach ($field->getColumns() as $column => $attributes) {
-              $column_name = static::_fieldColumnName($field, $column);
+            foreach ($storage_definition->getColumns() as $column => $attributes) {
+              $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
               // Unserialize the value if specified in the column schema.
               $item[$column] = (!empty($attributes['serialize'])) ? unserialize($row->$column_name) : $row->$column_name;
             }
@@ -1021,18 +1041,19 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) {
     $entity_type = $entity->getEntityTypeId();
     $default_langcode = $entity->getUntranslated()->language()->id;
     $translation_langcodes = array_keys($entity->getTranslationLanguages());
+    $table_mapping = $this->getTableMapping();
 
     if (!isset($vid)) {
       $vid = $id;
     }
 
-    foreach ($this->entityManager->getFieldDefinitions($entity_type, $bundle) as $field_name => $instance) {
-      if (!($instance instanceof FieldInstanceConfigInterface)) {
+    foreach ($this->entityManager->getFieldDefinitions($entity_type, $bundle) as $field_name => $field_definition) {
+      $storage_definition = $field_definition->getFieldStorageDefinition();
+      if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
         continue;
       }
-      $field = $instance->getFieldStorageDefinition();
-      $table_name = static::_fieldTableName($field);
-      $revision_name = static::_fieldRevisionTableName($field);
+      $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
+      $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
 
       // Delete and insert, rather than update, in case a value was added.
       if ($update) {
@@ -1052,13 +1073,13 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) {
       // Prepare the multi-insert query.
       $do_insert = FALSE;
       $columns = array('entity_id', 'revision_id', 'bundle', 'delta', 'langcode');
-      foreach ($field->getColumns() as $column => $attributes) {
-        $columns[] = static::_fieldColumnName($field, $column);
+      foreach ($storage_definition->getColumns() as $column => $attributes) {
+        $columns[] = $table_mapping->getFieldColumnName($storage_definition, $column);
       }
       $query = $this->database->insert($table_name)->fields($columns);
       $revision_query = $this->database->insert($revision_name)->fields($columns);
 
-      $langcodes = $field->isTranslatable() ? $translation_langcodes : array($default_langcode);
+      $langcodes = $field_definition->isTranslatable() ? $translation_langcodes : array($default_langcode);
       foreach ($langcodes as $langcode) {
         $delta_count = 0;
         $items = $entity->getTranslation($langcode)->get($field_name);
@@ -1073,15 +1094,15 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) {
             'delta' => $delta,
             'langcode' => $langcode,
           );
-          foreach ($field->getColumns() as $column => $attributes) {
-            $column_name = static::_fieldColumnName($field, $column);
+          foreach ($storage_definition->getColumns() as $column => $attributes) {
+            $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
             // Serialize the value if specified in the column schema.
             $record[$column_name] = !empty($attributes['serialize']) ? serialize($item->$column) : $item->$column;
           }
           $query->values($record);
           $revision_query->values($record);
 
-          if ($field->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $field->getCardinality()) {
+          if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) {
             break;
           }
         }
@@ -1103,13 +1124,14 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) {
    * {@inheritdoc}
    */
   protected function doDeleteFieldItems(EntityInterface $entity) {
-    foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $instance) {
-      if (!($instance instanceof FieldInstanceConfigInterface)) {
+    $table_mapping = $this->getTableMapping();
+    foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
+      $storage_definition = $field_definition->getFieldStorageDefinition();
+      if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
         continue;
       }
-      $field = $instance->getFieldStorageDefinition();
-      $table_name = static::_fieldTableName($field);
-      $revision_name = static::_fieldRevisionTableName($field);
+      $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
+      $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
       $this->database->delete($table_name)
         ->condition('entity_id', $entity->id())
         ->execute();
@@ -1125,11 +1147,13 @@ protected function doDeleteFieldItems(EntityInterface $entity) {
   protected function doDeleteFieldItemsRevision(EntityInterface $entity) {
     $vid = $entity->getRevisionId();
     if (isset($vid)) {
-      foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $instance) {
-        if (!($instance instanceof FieldInstanceConfigInterface)) {
+      $table_mapping = $this->getTableMapping();
+      foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
+        $storage_definition = $field_definition->getFieldStorageDefinition();
+        if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
           continue;
         }
-        $revision_name = static::_fieldRevisionTableName($instance->getFieldStorageDefinition());
+        $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
         $this->database->delete($revision_name)
           ->condition('entity_id', $entity->id())
           ->condition('revision_id', $vid)
@@ -1141,157 +1165,58 @@ protected function doDeleteFieldItemsRevision(EntityInterface $entity) {
   /**
    * {@inheritdoc}
    */
-  public function onFieldCreate(FieldConfigInterface $field) {
-    $schema = $this->_fieldSqlSchema($field);
-    foreach ($schema as $name => $table) {
-      $this->database->schema()->createTable($name, $table);
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
+    // If we are adding a field stored in a shared table we need to recompute
+    // the table mapping.
+    if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
+      $this->tableMapping = NULL;
     }
+    $this->schemaHandler()->createFieldSchema($storage_definition);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function onFieldUpdate(FieldConfigInterface $field) {
-    $original = $field->original;
-
-    if (!$field->hasData()) {
-      // There is no data. Re-create the tables completely.
-
-      if ($this->database->supportsTransactionalDDL()) {
-        // If the database supports transactional DDL, we can go ahead and rely
-        // on it. If not, we will have to rollback manually if something fails.
-        $transaction = $this->database->startTransaction();
-      }
-
-      try {
-        $original_schema = $this->_fieldSqlSchema($original);
-        foreach ($original_schema as $name => $table) {
-          $this->database->schema()->dropTable($name, $table);
-        }
-        $schema = $this->_fieldSqlSchema($field);
-        foreach ($schema as $name => $table) {
-          $this->database->schema()->createTable($name, $table);
-        }
-      }
-      catch (\Exception $e) {
-        if ($this->database->supportsTransactionalDDL()) {
-          $transaction->rollback();
-        }
-        else {
-          // Recreate tables.
-          $original_schema = $this->_fieldSqlSchema($original);
-          foreach ($original_schema as $name => $table) {
-            if (!$this->database->schema()->tableExists($name)) {
-              $this->database->schema()->createTable($name, $table);
-            }
-          }
-        }
-        throw $e;
-      }
-    }
-    else {
-      if ($field->getColumns() != $original->getColumns()) {
-        throw new FieldConfigUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data.");
-      }
-      // There is data, so there are no column changes. Drop all the prior
-      // indexes and create all the new ones, except for all the priors that
-      // exist unchanged.
-      $table = static::_fieldTableName($original);
-      $revision_table = static::_fieldRevisionTableName($original);
-
-      $schema = $field->getSchema();
-      $original_schema = $original->getSchema();
-
-      foreach ($original_schema['indexes'] as $name => $columns) {
-        if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) {
-          $real_name = static::_fieldIndexName($field, $name);
-          $this->database->schema()->dropIndex($table, $real_name);
-          $this->database->schema()->dropIndex($revision_table, $real_name);
-        }
-      }
-      $table = static::_fieldTableName($field);
-      $revision_table = static::_fieldRevisionTableName($field);
-      foreach ($schema['indexes'] as $name => $columns) {
-        if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) {
-          $real_name = static::_fieldIndexName($field, $name);
-          $real_columns = array();
-          foreach ($columns as $column_name) {
-            // Indexes can be specified as either a column name or an array with
-            // column name and length. Allow for either case.
-            if (is_array($column_name)) {
-              $real_columns[] = array(
-                static::_fieldColumnName($field, $column_name[0]),
-                $column_name[1],
-              );
-            }
-            else {
-              $real_columns[] = static::_fieldColumnName($field, $column_name);
-            }
-          }
-          $this->database->schema()->addIndex($table, $real_name, $real_columns);
-          $this->database->schema()->addIndex($revision_table, $real_name, $real_columns);
-        }
-      }
-    }
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    $this->schemaHandler()->updateFieldSchema($storage_definition, $original);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function onFieldDelete(FieldConfigInterface $field) {
-    // Mark all data associated with the field for deletion.
-    $table = static::_fieldTableName($field);
-    $revision_table = static::_fieldRevisionTableName($field);
-    $this->database->update($table)
-      ->fields(array('deleted' => 1))
-      ->execute();
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
+    $table_mapping = $this->getTableMapping();
 
-    // Move the table to a unique name while the table contents are being
-    // deleted.
-    $deleted_field = clone $field;
-    $deleted_field->deleted = TRUE;
-    $new_table = static::_fieldTableName($deleted_field);
-    $revision_new_table = static::_fieldRevisionTableName($deleted_field);
-    $this->database->schema()->renameTable($table, $new_table);
-    $this->database->schema()->renameTable($revision_table, $revision_new_table);
-  }
+    if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+      // Mark all data associated with the field for deletion.
+      $table = $table_mapping->getDedicatedDataTableName($storage_definition);
+      $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
+      $this->database->update($table)
+        ->fields(array('deleted' => 1))
+        ->execute();
+    }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function onInstanceDelete(FieldInstanceConfigInterface $instance) {
-    $field = $instance->getFieldStorageDefinition();
-    $table_name = static::_fieldTableName($field);
-    $revision_name = static::_fieldRevisionTableName($field);
-    $this->database->update($table_name)
-      ->fields(array('deleted' => 1))
-      ->condition('bundle', $instance->bundle)
-      ->execute();
-    $this->database->update($revision_name)
-      ->fields(array('deleted' => 1))
-      ->condition('bundle', $instance->bundle)
-      ->execute();
+    // Update the field schema.
+    $this->schemaHandler()->markFieldSchemaAsDeleted($storage_definition);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function onBundleRename($bundle, $bundle_new) {
-    // We need to account for deleted fields and instances. The method runs
-    // before the instance definitions are updated, so we need to fetch them
-    // using the old bundle name.
-    $instances = entity_load_multiple_by_properties('field_instance_config', array('entity_type' => $this->entityTypeId, 'bundle' => $bundle, 'include_deleted' => TRUE));
-    foreach ($instances as $instance) {
-      $field = $instance->getFieldStorageDefinition();
-      $table_name = static::_fieldTableName($field);
-      $revision_name = static::_fieldRevisionTableName($field);
+  public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {
+    $table_mapping = $this->getTableMapping();
+    $storage_definition = $field_definition->getFieldStorageDefinition();
+    // Mark field data as deleted.
+    if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+      $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
+      $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
       $this->database->update($table_name)
-        ->fields(array('bundle' => $bundle_new))
-        ->condition('bundle', $bundle)
+        ->fields(array('deleted' => 1))
+        ->condition('bundle', $field_definition->getBundle())
         ->execute();
       $this->database->update($revision_name)
-        ->fields(array('bundle' => $bundle_new))
-        ->condition('bundle', $bundle)
+        ->fields(array('deleted' => 1))
+        ->condition('bundle', $field_definition->getBundle())
         ->execute();
     }
   }
@@ -1299,348 +1224,157 @@ public function onBundleRename($bundle, $bundle_new) {
   /**
    * {@inheritdoc}
    */
-  protected function readFieldItemsToPurge(EntityInterface $entity, FieldInstanceConfigInterface $instance) {
-    $field = $instance->getFieldStorageDefinition();
-    $table_name = static::_fieldTableName($field);
-    $query = $this->database->select($table_name, 't', array('fetch' => \PDO::FETCH_ASSOC))
-      ->condition('entity_id', $entity->id())
-      ->orderBy('delta');
-    foreach ($field->getColumns() as $column_name => $data) {
-      $query->addField('t', static::_fieldColumnName($field, $column_name), $column_name);
-    }
-    return $query->execute()->fetchAll();
-  }
+  public function onBundleRename($bundle, $bundle_new) {
+    // The method runs before the field definitions are updated, so we use the
+    // old bundle name.
+    $field_definitions = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle);
+    // We need to handle deleted fields too. For now, this only makes sense for
+    // configurable fields, so we use the specific API.
+    // @todo Use the unified store of deleted field definitions instead in
+    //   https://www.drupal.org/node/2282119
+    $field_definitions += entity_load_multiple_by_properties('field_instance_config', array('entity_type' => $this->entityTypeId, 'bundle' => $bundle, 'deleted' => TRUE, 'include_deleted' => TRUE));
+    $table_mapping = $this->getTableMapping();
 
-  /**
-   * {@inheritdoc}
-   */
-  public function purgeFieldItems(EntityInterface $entity, FieldInstanceConfigInterface $instance) {
-    $field = $instance->getFieldStorageDefinition();
-    $table_name = static::_fieldTableName($field);
-    $revision_name = static::_fieldRevisionTableName($field);
-    $this->database->delete($table_name)
-      ->condition('entity_id', $entity->id())
-      ->execute();
-    $this->database->delete($revision_name)
-      ->condition('entity_id', $entity->id())
-      ->execute();
+    foreach ($field_definitions as $field_definition) {
+      $storage_definition = $field_definition->getFieldStorageDefinition();
+      if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+        $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
+        $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
+        $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
+        $this->database->update($table_name)
+          ->fields(array('bundle' => $bundle_new))
+          ->condition('bundle', $bundle)
+          ->execute();
+        $this->database->update($revision_name)
+          ->fields(array('bundle' => $bundle_new))
+          ->condition('bundle', $bundle)
+          ->execute();
+      }
+    }
   }
 
   /**
    * {@inheritdoc}
    */
-  public function onFieldPurge(FieldConfigInterface $field) {
-    $table_name = static::_fieldTableName($field);
-    $revision_name = static::_fieldRevisionTableName($field);
-    $this->database->schema()->dropTable($table_name);
-    $this->database->schema()->dropTable($revision_name);
-  }
-
-  /**
-   * Gets the SQL table schema.
-   *
-   * @private Calling this function circumvents the entity system and is
-   * strongly discouraged. This function is not considered part of the public
-   * API and modules relying on it might break even in minor releases.
-   *
-   * @param \Drupal\field\FieldConfigInterface $field
-   *   The field object
-   * @param array $schema
-   *   The field schema array. Mandatory for upgrades, omit otherwise.
-   *
-   * @return array
-   *   The same as a hook_schema() implementation for the data and the
-   *   revision tables.
-   *
-   * @see hook_schema()
-   */
-  public static function _fieldSqlSchema(FieldConfigInterface $field, array $schema = NULL) {
-    if ($field->deleted) {
-      $description_current = "Data storage for deleted field {$field->uuid()} ({$field->entity_type}, {$field->getName()}).";
-      $description_revision = "Revision archive storage for deleted field {$field->uuid()} ({$field->entity_type}, {$field->getName()}).";
-    }
-    else {
-      $description_current = "Data storage for {$field->entity_type} field {$field->getName()}.";
-      $description_revision = "Revision archive storage for {$field->entity_type} field {$field->getName()}.";
-    }
-
-    $entity_type_id = $field->entity_type;
-    $entity_manager = \Drupal::entityManager();
-    $entity_type = $entity_manager->getDefinition($entity_type_id);
-    $definitions = $entity_manager->getBaseFieldDefinitions($entity_type_id);
-
-    // Define the entity ID schema based on the field definitions.
-    $id_definition = $definitions[$entity_type->getKey('id')];
-    if ($id_definition->getType() == 'integer') {
-      $id_schema = array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => 'The entity id this data is attached to',
-      );
-    }
-    else {
-      $id_schema = array(
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => TRUE,
-        'description' => 'The entity id this data is attached to',
-      );
-    }
-
-    // Define the revision ID schema, default to integer if there is no revision
-    // ID.
-    $revision_id_definition = $entity_type->hasKey('revision') ? $definitions[$entity_type->getKey('revision')] : NULL;
-    if (!$revision_id_definition || $revision_id_definition->getType() == 'integer') {
-      $revision_id_schema = array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => FALSE,
-        'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned',
-      );
-    }
-    else {
-      $revision_id_schema = array(
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => FALSE,
-        'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned',
-      );
-    }
-
-    $current = array(
-      'description' => $description_current,
-      'fields' => array(
-        'bundle' => array(
-          'type' => 'varchar',
-          'length' => 128,
-          'not null' => TRUE,
-          'default' => '',
-          'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance',
-        ),
-        'deleted' => array(
-          'type' => 'int',
-          'size' => 'tiny',
-          'not null' => TRUE,
-          'default' => 0,
-          'description' => 'A boolean indicating whether this data item has been deleted'
-        ),
-        'entity_id' => $id_schema,
-        'revision_id' => $revision_id_schema,
-        'langcode' => array(
-          'type' => 'varchar',
-          'length' => 32,
-          'not null' => TRUE,
-          'default' => '',
-          'description' => 'The language code for this data item.',
-        ),
-        'delta' => array(
-          'type' => 'int',
-          'unsigned' => TRUE,
-          'not null' => TRUE,
-          'description' => 'The sequence number for this data item, used for multi-value fields',
-        ),
-      ),
-      'primary key' => array('entity_id', 'deleted', 'delta', 'langcode'),
-      'indexes' => array(
-        'bundle' => array('bundle'),
-        'deleted' => array('deleted'),
-        'entity_id' => array('entity_id'),
-        'revision_id' => array('revision_id'),
-        'langcode' => array('langcode'),
-      ),
-    );
+  protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) {
+    // Check whether the whole field storage definition is gone, or just some
+    // bundle fields.
+    $storage_definition = $field_definition->getFieldStorageDefinition();
+    $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
+    $table_mapping = $this->getTableMapping();
+    $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
 
-    if (!$schema) {
-      $schema = $field->getSchema();
+    // Get the entities which we want to purge first.
+    $entity_query = $this->database->select($table_name, 't', array('fetch' => \PDO::FETCH_ASSOC));
+    $or = $entity_query->orConditionGroup();
+    foreach ($storage_definition->getColumns() as $column_name => $data) {
+      $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
     }
+    $entity_query
+      ->distinct(TRUE)
+      ->fields('t', array('entity_id'))
+      ->condition('bundle', $field_definition->getBundle())
+      ->range(0, $batch_size);
 
-    // Add field columns.
-    foreach ($schema['columns'] as $column_name => $attributes) {
-      $real_name = static::_fieldColumnName($field, $column_name);
-      $current['fields'][$real_name] = $attributes;
+    // Create a map of field data table column names to field column names.
+    $column_map = array();
+    foreach ($storage_definition->getColumns() as $column_name => $data) {
+      $column_map[$table_mapping->getFieldColumnName($storage_definition, $column_name)] = $column_name;
     }
 
-    // Add unique keys.
-    foreach ($schema['unique keys'] as $unique_key_name => $columns) {
-      $real_name = static::_fieldIndexName($field, $unique_key_name);
-      foreach ($columns as $column_name) {
-        $current['unique keys'][$real_name][] = static::_fieldColumnName($field, $column_name);
-      }
-    }
-
-    // Add indexes.
-    foreach ($schema['indexes'] as $index_name => $columns) {
-      $real_name = static::_fieldIndexName($field, $index_name);
-      foreach ($columns as $column_name) {
-        // Indexes can be specified as either a column name or an array with
-        // column name and length. Allow for either case.
-        if (is_array($column_name)) {
-          $current['indexes'][$real_name][] = array(
-            static::_fieldColumnName($field, $column_name[0]),
-            $column_name[1],
-          );
+    $entities = array();
+    $items_by_entity = array();
+    foreach ($entity_query->execute() as $row) {
+      $item_query = $this->database->select($table_name, 't', array('fetch' => \PDO::FETCH_ASSOC))
+        ->fields('t')
+        ->condition('entity_id', $row['entity_id'])
+        ->orderBy('delta');
+
+      foreach ($item_query->execute() as $item_row) {
+        if (!isset($entities[$item_row['revision_id']])) {
+          // Create entity with the right revision id and entity id combination.
+          $item_row['entity_type'] = $this->entityTypeId;
+          // @todo: Replace this by an entity object created via an entity
+          // factory, see https://drupal.org/node/1867228.
+          $entities[$item_row['revision_id']] = _field_create_entity_from_ids((object) $item_row);
         }
-        else {
-          $current['indexes'][$real_name][] = static::_fieldColumnName($field, $column_name);
+        $item = array();
+        foreach ($column_map as $db_column => $field_column) {
+          $item[$field_column] = $item_row[$db_column];
         }
+        $items_by_entity[$item_row['revision_id']][] = $item;
       }
     }
 
-    // Add foreign keys.
-    foreach ($schema['foreign keys'] as $specifier => $specification) {
-      $real_name = static::_fieldIndexName($field, $specifier);
-      $current['foreign keys'][$real_name]['table'] = $specification['table'];
-      foreach ($specification['columns'] as $column_name => $referenced) {
-        $sql_storage_column = static::_fieldColumnName($field, $column_name);
-        $current['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced;
-      }
-    }
-
-    // Construct the revision table.
-    $revision = $current;
-    $revision['description'] = $description_revision;
-    $revision['primary key'] = array('entity_id', 'revision_id', 'deleted', 'delta', 'langcode');
-    $revision['fields']['revision_id']['not null'] = TRUE;
-    $revision['fields']['revision_id']['description'] = 'The entity revision id this data is attached to';
-
-    return array(
-      static::_fieldTableName($field) => $current,
-      static::_fieldRevisionTableName($field) => $revision,
-    );
-  }
-
-  /**
-   * Generates a table name for a field data table.
-   *
-   * @private Calling this function circumvents the entity system and is
-   * strongly discouraged. This function is not considered part of the public
-   * API and modules relying on it might break even in minor releases. Only
-   * call this function to write a query that \Drupal::entityQuery() does not
-   * support. Always call entity_load() before using the data found in the
-   * table.
-   *
-   * @param \Drupal\field\FieldConfigInterface $field
-   *   The field object.
-   *
-   * @return string
-   *   A string containing the generated name for the database table.
-   *
-   */
-  static public function _fieldTableName(FieldConfigInterface $field) {
-    if ($field->deleted) {
-      // When a field is a deleted, the table is renamed to
-      // {field_deleted_data_FIELD_UUID}. To make sure we don't end up with
-      // table names longer than 64 characters, we hash the uuid and return the
-      // first 10 characters so we end up with a short unique ID.
-      return "field_deleted_data_" . substr(hash('sha256', $field->uuid()), 0, 10);
-    }
-    else {
-      return static::_generateFieldTableName($field, FALSE);
+    // Create field item objects and return.
+    foreach ($items_by_entity as $revision_id => $values) {
+      $items_by_entity[$revision_id] = \Drupal::typedDataManager()->create($field_definition, $values, $field_definition->getName(), $entities[$revision_id]);
     }
+    return $items_by_entity;
   }
 
   /**
-   * Generates a table name for a field revision archive table.
-   *
-   * @private Calling this function circumvents the entity system and is
-   * strongly discouraged. This function is not considered part of the public
-   * API and modules relying on it might break even in minor releases. Only
-   * call this function to write a query that \Drupal::entityQuery() does not
-   * support. Always call entity_load() before using the data found in the
-   * table.
-   *
-   * @param \Drupal\field\FieldConfigInterface $field
-   *   The field object.
-   *
-   * @return string
-   *   A string containing the generated name for the database table.
+   * {@inheritdoc}
    */
-  static public function _fieldRevisionTableName(FieldConfigInterface $field) {
-    if ($field->deleted) {
-      // When a field is a deleted, the table is renamed to
-      // {field_deleted_revision_FIELD_UUID}. To make sure we don't end up with
-      // table names longer than 64 characters, we hash the uuid and return the
-      // first 10 characters so we end up with a short unique ID.
-      return "field_deleted_revision_" . substr(hash('sha256', $field->uuid()), 0, 10);
-    }
-    else {
-      return static::_generateFieldTableName($field, TRUE);
-    }
+  protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {
+    $storage_definition = $field_definition->getFieldStorageDefinition();
+    $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
+    $table_mapping = $this->getTableMapping();
+    $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
+    $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
+    $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id();
+    $this->database->delete($table_name)
+      ->condition('revision_id', $revision_id)
+      ->execute();
+    $this->database->delete($revision_name)
+      ->condition('revision_id', $revision_id)
+      ->execute();
   }
 
   /**
-   * Generates a safe and unanbiguous field table name.
-   *
-   * The method accounts for a maximum table name length of 64 characters, and
-   * takes care of disambiguation.
-   *
-   * @param \Drupal\field\FieldConfigInterface $field
-   *   The field object.
-   * @param bool $revision
-   *   TRUE for revision table, FALSE otherwise.
-   *
-   * @return string
-   *   The final table name.
+   * {@inheritdoc}
    */
-  static protected function _generateFieldTableName(FieldConfigInterface $field, $revision) {
-    $separator = $revision ? '_revision__' : '__';
-    $table_name = $field->entity_type . $separator .  $field->name;
-    // Limit the string to 48 characters, keeping a 16 characters margin for db
-    // prefixes.
-    if (strlen($table_name) > 48) {
-      // Use a shorter separator, a truncated entity_type, and a hash of the
-      // field UUID.
-      $separator = $revision ? '_r__' : '__';
-      // Truncate to the same length for the current and revision tables.
-      $entity_type = substr($field->entity_type, 0, 34);
-      $field_hash = substr(hash('sha256', $field->uuid()), 0, 10);
-      $table_name = $entity_type . $separator . $field_hash;
-    }
-    return $table_name;
+  public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
+    $this->schemaHandler()->deleteFieldSchema($storage_definition);
   }
 
   /**
-   * Generates an index name for a field data table.
-   *
-   * @private Calling this function circumvents the entity system and is
-   * strongly discouraged. This function is not considered part of the public
-   * API and modules relying on it might break even in minor releases.
-   *
-   * @param \Drupal\field\FieldConfigInterface $field
-   *   The field structure
-   * @param string $index
-   *   The name of the index.
-   *
-   * @return string
-   *   A string containing a generated index name for a field data table that is
-   *   unique among all other fields.
+   * {@inheritdoc}
    */
-  static public function _fieldIndexName(FieldConfigInterface $field, $index) {
-    return $field->getName() . '_' . $index;
+  public function countFieldData($storage_definition, $as_bool = FALSE) {
+    $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
+    $table_mapping = $this->getTableMapping();
+    $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
+
+    $query = $this->database->select($table_name, 't');
+    $or = $query->orConditionGroup();
+    foreach ($storage_definition->getColumns() as $column_name => $data) {
+      $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
+    }
+    $query
+      ->condition($or)
+      ->fields('t', array('entity_id'))
+      ->distinct(TRUE);
+    // If we are performing the query just to check if the field has data
+    // limit the number of rows.
+    if ($as_bool) {
+      $query->range(0, 1);
+    }
+    $count = $query->countQuery()->execute()->fetchField();
+    return $as_bool ? (bool) $count : (int) $count;
   }
 
   /**
-   * Generates a column name for a field data table.
+   * Returns whether the passed field has been already deleted.
    *
-   * @private Calling this function circumvents the entity system and is
-   * strongly discouraged. This function is not considered part of the public
-   * API and modules relying on it might break even in minor releases. Only
-   * call this function to write a query that \Drupal::entityQuery() does not
-   * support. Always call entity_load() before using the data found in the
-   * table.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
    *
-   * @param \Drupal\field\FieldConfigInterface $field
-   *   The field object.
-   * @param string $column
-   *   The name of the column.
-   *
-   * @return string
-   *   A string containing a generated column name for a field data table that is
-   *   unique among all other fields.
+   * @return bool
+   *   Whether the field has been already deleted.
    */
-  static public function _fieldColumnName(FieldConfigInterface $field, $column) {
-    return in_array($column, FieldConfig::getReservedColumns()) ? $column : $field->getName() . '_' . $column;
+  protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) {
+    return !array_key_exists($storage_definition->getName(), $this->entityManager->getFieldStorageDefinitions($this->entityTypeId));
   }
 
 }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
index 98dbd2f..12aeca3 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
@@ -8,7 +8,7 @@
 namespace Drupal\Core\Entity;
 
 use Drupal\Core\Entity\Query\QueryException;
-use Drupal\field\FieldInstanceConfigInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
 
 /**
  * Defines a null entity storage.
@@ -109,13 +109,14 @@ protected function doDeleteFieldItemsRevision(EntityInterface $entity) {
   /**
    * {@inheritdoc}
    */
-  protected function readFieldItemsToPurge(EntityInterface $entity, FieldInstanceConfigInterface $instance) {
+  protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) {
+    return array();
   }
 
   /**
    * {@inheritdoc}
    */
-  protected function purgeFieldItems(EntityInterface $entity, FieldInstanceConfigInterface $instance) {
+  protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {
   }
 
   /**
@@ -130,4 +131,11 @@ protected function doSave($id, EntityInterface $entity) {
   protected function has($id, EntityInterface $entity) {
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function countFieldData($storage_definition, $as_bool = FALSE) {
+    return $as_bool ? FALSE : 0;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
index 29c1de9..dad9674 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -8,8 +8,9 @@
 namespace Drupal\Core\Entity;
 
 use Drupal\Component\Utility\String;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Cache\Cache;
-use Drupal\field\FieldConfigInterface;
 use Drupal\field\FieldInstanceConfigInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -267,32 +268,32 @@ protected function deleteFieldItemsRevision(EntityInterface $entity) {
   /**
    * {@inheritdoc}
    */
-  public function onFieldCreate(FieldConfigInterface $field) { }
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { }
 
   /**
    * {@inheritdoc}
    */
-  public function onFieldUpdate(FieldConfigInterface $field) { }
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { }
 
   /**
    * {@inheritdoc}
    */
-  public function onFieldDelete(FieldConfigInterface $field) { }
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { }
 
   /**
    * {@inheritdoc}
    */
-  public function onInstanceCreate(FieldInstanceConfigInterface $instance) { }
+  public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition) { }
 
   /**
    * {@inheritdoc}
    */
-  public function onInstanceUpdate(FieldInstanceConfigInterface $instance) { }
+  public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original) { }
 
   /**
    * {@inheritdoc}
    */
-  public function onInstanceDelete(FieldInstanceConfigInterface $instance) { }
+  public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) { }
 
   /**
    * {@inheritdoc}
@@ -312,45 +313,46 @@ public function onBundleDelete($bundle) { }
   /**
    * {@inheritdoc}
    */
-  public function onFieldItemsPurge(EntityInterface $entity, FieldInstanceConfigInterface $instance) {
-    if ($values = $this->readFieldItemsToPurge($entity, $instance)) {
-      $items = \Drupal::typedDataManager()->create($instance, $values, $instance->getName(), $entity);
+  public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size) {
+    $items_by_entity = $this->readFieldItemsToPurge($field_definition, $batch_size);
+
+    foreach ($items_by_entity as $items) {
       $items->delete();
+      $this->purgeFieldItems($items->getEntity(), $field_definition);
     }
-    $this->purgeFieldItems($entity, $instance);
+    return count($items_by_entity);
   }
 
   /**
-   * Reads values to be purged for a single field of a single entity.
+   * Reads values to be purged for a single field.
    *
    * This method is called during field data purge, on fields for which
    * onFieldDelete() or onFieldInstanceDelete() has previously run.
    *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   The entity.
-   * @param \Drupal\field\FieldInstanceConfigInterface $instance
-   *   The field instance.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The field definition.
+   * @param $batch_size
+   *   The maximum number of field data records to purge before returning.
    *
-   * @return array
-   *   The field values, in their canonical array format (numerically indexed
-   *   array of items, each item being a property/value array).
+   * @return \Drupal\Core\Field\FieldItemListInterface[]
+   *   An array of field item lists, keyed by entity revision id.
    */
-  abstract protected function readFieldItemsToPurge(EntityInterface $entity, FieldInstanceConfigInterface $instance);
+  abstract protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size);
 
   /**
-   * Removes field data from storage during purge.
+   * Removes field items from storage per entity during purge.
    *
-   * @param EntityInterface $entity
-   *   The entity whose values are being purged.
-   * @param FieldInstanceConfigInterface $instance
+   * @param ContentEntityInterface $entity
+   *   The entity revision, whose values are being purged.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
    *   The field whose values are bing purged.
    */
-  abstract protected function purgeFieldItems(EntityInterface $entity, FieldInstanceConfigInterface $instance);
+  abstract protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition);
 
   /**
    * {@inheritdoc}
    */
-  public function onFieldPurge(FieldConfigInterface $field) { }
+  public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) { }
 
   /**
    * Checks translation statuses and invoke the related hooks if needed.
diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php
index cf5ae6b..ba644be 100644
--- a/core/lib/Drupal/Core/Entity/EntityManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityManager.php
@@ -394,11 +394,13 @@ protected function buildBaseFieldDefinitions($entity_type_id) {
       }
     }
 
-    // Automatically set the field name for non-configurable fields.
+    // Automatically set the field name, target entity type and bundle
+    // for non-configurable fields.
     foreach ($base_field_definitions as $field_name => $base_field_definition) {
       if ($base_field_definition instanceof FieldDefinition) {
         $base_field_definition->setName($field_name);
         $base_field_definition->setTargetEntityTypeId($entity_type_id);
+        $base_field_definition->setBundle(NULL);
       }
     }
 
@@ -491,11 +493,13 @@ protected function buildBundleFieldDefinitions($entity_type_id, $bundle, array $
       }
     }
 
-    // Automatically set the field name for non-configurable fields.
+    // Automatically set the field name, target entity type and bundle
+    // for non-configurable fields.
     foreach ($bundle_field_definitions as $field_name => $field_definition) {
       if ($field_definition instanceof FieldDefinition) {
         $field_definition->setName($field_name);
         $field_definition->setTargetEntityTypeId($entity_type_id);
+        $field_definition->setBundle($bundle);
       }
     }
 
diff --git a/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php b/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php
new file mode 100644
index 0000000..54e2bff
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Exception\StorageDefinitionUpdateForbiddenException.
+ */
+
+namespace Drupal\Core\Entity\Exception;
+
+/**
+ * Exception thrown when a storage definition update is forbidden.
+ */
+class FieldStorageDefinitionUpdateForbiddenException extends \Exception { }
diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
index 2998c21..2d695c3 100644
--- a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
@@ -7,71 +7,78 @@
 
 namespace Drupal\Core\Entity;
 
-use Drupal\field\FieldConfigInterface;
-use Drupal\field\FieldInstanceConfigInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 interface FieldableEntityStorageInterface extends EntityStorageInterface {
 
   /**
-   * Allows reaction to the creation of a configurable field.
+   * Reacts to the creation of a field storage definition.
    *
-   * @param \Drupal\field\FieldConfigInterface $field
-   *   The field being created.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The definition being created.
    */
-  public function onFieldCreate(FieldConfigInterface $field);
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition);
 
   /**
-   * Allows reaction to the update of a configurable field.
+   * Reacts to the update of a field storage definition.
    *
-   * @param \Drupal\field\FieldConfigInterface $field
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
    *   The field being updated.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original storage definition; i.e., the definition before the update.
+   *
+   * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
+   *   Thrown when the update to the field is forbidden.
    */
-  public function onFieldUpdate(FieldConfigInterface $field);
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original);
 
   /**
-   * Allows reaction to the deletion of a configurable field.
+   * Reacts to the deletion of a field storage definition.
    *
    * Stored values should not be wiped at once, but marked as 'deleted' so that
    * they can go through a proper purge process later on.
    *
-   * @param \Drupal\field\FieldConfigInterface $field
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
    *   The field being deleted.
    *
-   * @see fieldPurgeData()
+   * @see purgeFieldData()
    */
-  public function onFieldDelete(FieldConfigInterface $field);
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition);
 
   /**
-   * Allows reaction to the creation of a configurable field instance.
+   * Reacts to the creation of a field.
    *
-   * @param \Drupal\field\FieldInstanceConfigInterface $instance
-   *   The instance being created.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The field definition created.
    */
-  public function onInstanceCreate(FieldInstanceConfigInterface $instance);
+  public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition);
 
   /**
-   * Allows reaction to the update of a configurable field instance.
+   * Reacts to the update of a field.
    *
-   * @param \Drupal\field\FieldInstanceConfigInterface $instance
-   *   The instance being updated.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The field definition being updated.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The original field definition; i.e., the definition before the update.
    */
-  public function onInstanceUpdate(FieldInstanceConfigInterface $instance);
+  public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original);
 
   /**
-   * Allows reaction to the deletion of a configurable field instance.
+   * Reacts to the deletion of a field.
    *
    * Stored values should not be wiped at once, but marked as 'deleted' so that
    * they can go through a proper purge process later on.
    *
-   * @param \Drupal\field\FieldInstanceConfigInterface $instance
-   *   The instance being deleted.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The field definition being deleted.
    *
-   * @see fieldPurgeData()
+   * @see purgeFieldData()
    */
-  public function onInstanceDelete(FieldInstanceConfigInterface $instance);
+  public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition);
 
   /**
-   * Allows reaction to a bundle being created.
+   * Reacts to a bundle being created.
    *
    * @param string $bundle
    *   The name of the bundle created.
@@ -79,10 +86,9 @@ public function onInstanceDelete(FieldInstanceConfigInterface $instance);
   public function onBundleCreate($bundle);
 
   /**
-   * Allows reaction to a bundle being renamed.
+   * Reacts to a bundle being renamed.
    *
-   * This method runs before field instance definitions are updated with the new
-   * bundle name.
+   * This method runs before fields are updated with the new bundle name.
    *
    * @param string $bundle
    *   The name of the bundle being renamed.
@@ -92,9 +98,9 @@ public function onBundleCreate($bundle);
   public function onBundleRename($bundle, $bundle_new);
 
   /**
-   * Allows reaction to a bundle being deleted.
+   * Reacts to a bundle being deleted.
    *
-   * This method runs before field and instance definitions are deleted.
+   * This method runs before fields are deleted.
    *
    * @param string $bundle
    *   The name of the bundle being deleted.
@@ -102,24 +108,43 @@ public function onBundleRename($bundle, $bundle_new);
   public function onBundleDelete($bundle);
 
   /**
-   * Purges the field data for a single field on a single entity.
+   * Purges a batch of field data.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The deleted field whose data is being purged.
+   * @param $batch_size
+   *   The maximum number of field data records to purge before returning,
+   *   relating to the count of field data records returned by
+   *   \Drupal\Core\Entity\FieldableEntityStorageInterface::countFieldData().
+   *
+   * @return int
+   *   The number of field data records that have been purged.
+   */
+  public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size);
+
+  /**
+   * Determines the number of entities with values for a given field.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field for which to count data records.
+   * @param bool $as_bool
+   *   (Optional) Optimises the query for checking whether there are any records
+   *   or not. Defaults to FALSE.
    *
-   * The entity itself is not being deleted, and it is quite possible that
-   * other field data will remain attached to it.
+   * @return bool|int
+   *   The number of entities. If $as_bool parameter is TRUE then the
+   *   value will either be TRUE or FALSE.
    *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   The entity whose field data is being purged.
-   * @param \Drupal\field\FieldInstanceConfigInterface $instance
-   *   The deleted field instance whose data is being purged.
+   * @see \Drupal\Core\Entity\FieldableEntityStorageInterface::purgeFieldData()
    */
-  public function onFieldItemsPurge(EntityInterface $entity, FieldInstanceConfigInterface $instance);
+  public function countFieldData($storage_definition, $as_bool = FALSE);
 
   /**
-   * Performs final cleanup after all data on all instances has been purged.
+   * Performs final cleanup after all data of a field has been purged.
    *
-   * @param \Drupal\field\FieldConfigInterface $instance
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
    *   The field being purged.
    */
-  public function onFieldPurge(FieldConfigInterface $field);
+  public function finalizePurge(FieldStorageDefinitionInterface $storage_definition);
 
 }
diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
index 6926603..ae7c13e 100644
--- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
@@ -106,29 +106,25 @@ public function addField($field, $type, $langcode) {
       // This can either be the name of an entity base field or a configurable
       // field.
       $specifier = $specifiers[$key];
-      // Normally it is a field name, but field_purge_batch() is passing in
-      // id:$field_id so check that first.
-      /* @var \Drupal\Core\Field\FieldDefinitionInterface $field */
-      if (substr($specifier, 0, 3) == 'id:') {
-        if ($fields = entity_load_multiple_by_properties('field_config', array('uuid' => substr($specifier, 3), 'include_deleted' => TRUE))) {
-          $field = current($fields);
-        }
-      }
-      elseif (isset($field_storage_definitions[$specifier])) {
+      if (isset($field_storage_definitions[$specifier])) {
         $field = $field_storage_definitions[$specifier];
       }
       else {
         $field = FALSE;
       }
+
       // If we managed to retrieve a configurable field, process it.
       if ($field instanceof FieldConfigInterface) {
         // Find the field column.
         $column = $field->getMainPropertyName();
+        /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+        $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping();
+
         if ($key < $count) {
           $next = $specifiers[$key + 1];
           // Is this a field column?
           $columns = $field->getColumns();
-          if (isset($columns[$next]) || in_array($next, FieldConfig::getReservedColumns())) {
+          if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) {
             // Use it.
             $column = $next;
             // Do not process it again.
@@ -150,7 +146,7 @@ public function addField($field, $type, $langcode) {
           }
         }
         $table = $this->ensureFieldTable($index_prefix, $field, $type, $langcode, $base_table, $entity_id_field, $field_id_field);
-        $sql_column = ContentEntityDatabaseStorage::_fieldColumnName($field, $column);
+        $sql_column = $table_mapping->getFieldColumnName($field, $column);
       }
       // This is an entity base field (non-configurable field).
       else {
@@ -228,11 +224,13 @@ protected function ensureEntityTable($index_prefix, $property, $type, $langcode,
   protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field) {
     $field_name = $field->getName();
     if (!isset($this->fieldTables[$index_prefix . $field_name])) {
-      $table = $this->sqlQuery->getMetaData('age') == EntityStorageInterface::FIELD_LOAD_CURRENT ? ContentEntityDatabaseStorage::_fieldTableName($field) : ContentEntityDatabaseStorage::_fieldRevisionTableName($field);
+      $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
+      /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+      $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping();
+      $table = $this->sqlQuery->getMetaData('age') == EntityStorageInterface::FIELD_LOAD_CURRENT ? $table_mapping->getDedicatedDataTableName($field) : $table_mapping->getDedicatedRevisionTableName($field);
       if ($field->getCardinality() != 1) {
         $this->sqlQuery->addMetaData('simple_query', FALSE);
       }
-      $entity_type = $this->sqlQuery->getMetaData('entity_type');
       $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode);
     }
     return $this->fieldTables[$index_prefix . $field_name];
diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
index 2294794..272a3ab 100644
--- a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
+++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
@@ -7,9 +7,12 @@
 
 namespace Drupal\Core\Entity\Schema;
 
+use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * Defines a schema handler that supports revisionable, translatable entities.
@@ -45,6 +48,13 @@ class ContentEntitySchemaHandler implements EntitySchemaHandlerInterface {
   protected $schema;
 
   /**
+   * The database connection to be used.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
    * Constructs a ContentEntitySchemaHandler.
    *
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
@@ -53,11 +63,343 @@ class ContentEntitySchemaHandler implements EntitySchemaHandlerInterface {
    *   The entity type.
    * @param \Drupal\Core\Entity\ContentEntityDatabaseStorage $storage
    *   The storage of the entity type. This must be an SQL-based storage.
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database connection to be used.
    */
-  public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorage $storage) {
+  public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorage $storage, Connection $database) {
     $this->entityType = $entity_type;
     $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id());
     $this->storage = $storage;
+    $this->database = $database;
+  }
+
+  /**
+   * Performs the specified operation on a field.
+   *
+   * This figures out whether the field is stored in a dedicated or shared table
+   * and forwards the call to the proper handler.
+   *
+   * @param string $operation
+   *   The name of the operation to be performed.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   (optional) The original field storage definition. This is relevant (and
+   *   required) only for updates. Defaults to NULL.
+   */
+  protected function performSchemaOperation($operation, FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original = NULL) {
+    $table_mapping = $this->storage->getTableMapping();
+    if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+      $this->{$operation . 'DedicatedTableSchema'}($storage_definition, $original);
+    }
+    elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
+      $this->{$operation . 'SharedTableSchema'}($storage_definition, $original);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createFieldSchema(FieldStorageDefinitionInterface $storage_definition) {
+    $this->performSchemaOperation('create', $storage_definition);
+  }
+
+  /**
+   * Creates the schema for a field stored in a dedicated table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being created.
+   */
+  protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
+    $schema = $this->getDedicatedTableSchema($storage_definition);
+    foreach ($schema as $name => $table) {
+      $this->database->schema()->createTable($name, $table);
+    }
+  }
+
+  /**
+   * Creates the schema for a field stored in a shared table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being created.
+   */
+  protected function createSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
+    $created_field_name = $storage_definition->getName();
+    $table_mapping = $this->storage->getTableMapping();
+    $column_names = $table_mapping->getColumnNames($created_field_name);
+    $schema = $this->getSharedTableFieldSchema($storage_definition, $column_names);
+    $keys = array_diff_key($schema, array('fields' => FALSE));
+
+    // Iterate over the mapped table to find the ones that will host the created
+    // field schema.
+    foreach ($table_mapping->getTableNames() as $table_name) {
+      foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
+        if ($field_name == $created_field_name) {
+          foreach ($schema['fields'] as $column_name => $specifier) {
+            $this->database->schema()->addField($table_name, $column_name, $specifier, $keys);
+          }
+          // After creating the field schema skip to the next table.
+          break;
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function markFieldSchemaAsDeleted(FieldStorageDefinitionInterface $storage_definition) {
+    $table_mapping = $this->storage->getTableMapping();
+    // TODO Do we need this also for shared table storage?
+    if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+      // Move the table to a unique name while the table contents are being
+      // deleted.
+      $table = $table_mapping->getDedicatedDataTableName($storage_definition);
+      $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
+      $new_table = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
+      $revision_new_table = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE);
+      $this->database->schema()->renameTable($table, $new_table);
+      $this->database->schema()->renameTable($revision_table, $revision_new_table);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteFieldSchema(FieldStorageDefinitionInterface $storage_definition) {
+    $this->performSchemaOperation('delete', $storage_definition);
+  }
+
+  /**
+   * Deletes the schema for a field stored in a dedicated table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being deleted.
+   */
+  protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
+    $table_mapping = $this->storage->getTableMapping();
+    $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
+    $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE);
+    $this->database->schema()->dropTable($table_name);
+    $this->database->schema()->dropTable($revision_name);
+  }
+
+  /**
+   * Deletes the schema for a field stored in a shared table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being deleted.
+   */
+  protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
+    $deleted_field_name = $storage_definition->getName();
+    $table_mapping = $this->storage->getTableMapping();
+    $column_names = $table_mapping->getColumnNames($deleted_field_name);
+    $schema = $this->getSharedTableFieldSchema($storage_definition, $column_names);
+    $schema_handler = $this->database->schema();
+
+    // Iterate over the mapped table to find the ones that host the deleted
+    // field schema.
+    foreach ($table_mapping->getTableNames() as $table_name) {
+      foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
+        if ($field_name == $deleted_field_name) {
+          // Drop indexes and unique keys first.
+          if (!empty($schema['indexes'])) {
+            foreach ($schema['indexes'] as $name => $specifier) {
+              $schema_handler->dropIndex($table_name, $name);
+            }
+          }
+          if (!empty($schema['unique keys'])) {
+            foreach ($schema['unique keys'] as $name => $specifier) {
+              $schema_handler->dropUniqueKey($table_name, $name);
+            }
+          }
+          // Drop columns.
+          foreach ($column_names as $column_name) {
+            $schema_handler->dropField($table_name, $column_name);
+          }
+          // After deleting the field schema skip to the next table.
+          break;
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateFieldSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    $this->performSchemaOperation('update', $storage_definition, $original);
+  }
+
+  /**
+   * Updates the schema for a field stored in a shared table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being updated.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original storage definition; i.e., the definition before the update.
+   *
+   * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
+   *   Thrown when the update to the field is forbidden.
+   */
+  protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    if (!$storage_definition->hasData()) {
+      // There is no data. Re-create the tables completely.
+      if ($this->database->supportsTransactionalDDL()) {
+        // If the database supports transactional DDL, we can go ahead and rely
+        // on it. If not, we will have to rollback manually if something fails.
+        $transaction = $this->database->startTransaction();
+      }
+      try {
+        $original_schema = $this->getDedicatedTableSchema($original);
+        foreach ($original_schema as $name => $table) {
+          $this->database->schema()->dropTable($name, $table);
+        }
+        $schema = $this->getDedicatedTableSchema($storage_definition);
+        foreach ($schema as $name => $table) {
+          $this->database->schema()->createTable($name, $table);
+        }
+      }
+      catch (\Exception $e) {
+        if ($this->database->supportsTransactionalDDL()) {
+          $transaction->rollback();
+        }
+        else {
+          // Recreate tables.
+          $original_schema = $this->getDedicatedTableSchema($original);
+          foreach ($original_schema as $name => $table) {
+            if (!$this->database->schema()->tableExists($name)) {
+              $this->database->schema()->createTable($name, $table);
+            }
+          }
+        }
+        throw $e;
+      }
+    }
+    else {
+      if ($storage_definition->getColumns() != $original->getColumns()) {
+        throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data.");
+      }
+      // There is data, so there are no column changes. Drop all the prior
+      // indexes and create all the new ones, except for all the priors that
+      // exist unchanged.
+      $table_mapping = $this->storage->getTableMapping();
+      $table = $table_mapping->getDedicatedDataTableName($original);
+      $revision_table = $table_mapping->getDedicatedRevisionTableName($original);
+
+      $schema = $storage_definition->getSchema();
+      $original_schema = $original->getSchema();
+
+      foreach ($original_schema['indexes'] as $name => $columns) {
+        if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) {
+          $real_name = $this->getFieldIndexName($storage_definition, $name);
+          $this->database->schema()->dropIndex($table, $real_name);
+          $this->database->schema()->dropIndex($revision_table, $real_name);
+        }
+      }
+      $table = $table_mapping->getDedicatedDataTableName($storage_definition);
+      $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
+      foreach ($schema['indexes'] as $name => $columns) {
+        if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) {
+          $real_name = $this->getFieldIndexName($storage_definition, $name);
+          $real_columns = array();
+          foreach ($columns as $column_name) {
+            // Indexes can be specified as either a column name or an array with
+            // column name and length. Allow for either case.
+            if (is_array($column_name)) {
+              $real_columns[] = array(
+                $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
+                $column_name[1],
+              );
+            }
+            else {
+              $real_columns[] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
+            }
+          }
+          $this->database->schema()->addIndex($table, $real_name, $real_columns);
+          $this->database->schema()->addIndex($revision_table, $real_name, $real_columns);
+        }
+      }
+    }
+  }
+
+  /**
+   * Updates the schema for a field stored in a shared table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being updated.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original storage definition; i.e., the definition before the update.
+   */
+  protected function updateSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    if (!$this->storage->countFieldData($storage_definition, TRUE)) {
+      if ($this->database->supportsTransactionalDDL()) {
+        // If the database supports transactional DDL, we can go ahead and rely
+        // on it. If not, we will have to rollback manually if something fails.
+        $transaction = $this->database->startTransaction();
+      }
+      try {
+        $this->deleteSharedTableSchema($original);
+        $this->createSharedTableSchema($storage_definition);
+      }
+      catch (\Exception $e) {
+        if ($this->database->supportsTransactionalDDL()) {
+          $transaction->rollback();
+        }
+        else {
+          // Recreate original schema.
+          $this->createSharedTableSchema($original);
+        }
+        throw $e;
+      }
+    }
+    else {
+      if ($storage_definition->getColumns() != $original->getColumns()) {
+        throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data.");
+      }
+
+      $schema = array();
+      $original_schema = array();
+      $updated_field_name = $storage_definition->getName();
+      $table_mapping = $this->storage->getTableMapping();
+      $column_names = $table_mapping->getColumnNames($updated_field_name);
+      $original_schema = $this->getSharedTableFieldSchema($original, $column_names);
+      $schema = $this->getSharedTableFieldSchema($storage_definition, $column_names);
+      $schema_handler = $this->database->schema();
+
+      // Iterate over the mapped table to find the ones that host the deleted
+      // field schema.
+      foreach ($table_mapping->getTableNames() as $table_name) {
+        foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
+          if ($field_name == $updated_field_name) {
+            // Drop original indexes and unique keys.
+            if (!empty($original_schema['indexes'])) {
+              foreach ($original_schema['indexes'] as $name => $specifier) {
+                $schema_handler->dropIndex($table_name, $name);
+              }
+            }
+            if (!empty($original_schema['unique keys'])) {
+              foreach ($original_schema['unique keys'] as $name => $specifier) {
+                $schema_handler->dropUniqueKey($table_name, $name);
+              }
+            }
+            // Create new indexes and unique keys.
+            if (!empty($schema['indexes'])) {
+              foreach ($schema['indexes'] as $name => $specifier) {
+                $schema_handler->addIndex($table_name, $name, $specifier);
+              }
+            }
+            if (!empty($schema['unique keys'])) {
+              foreach ($schema['unique keys'] as $name => $specifier) {
+                $schema_handler->addUniqueKey($table_name, $name, $specifier);
+              }
+            }
+            // After deleting the field schema skip to the next table.
+            break;
+          }
+        }
+      }
+    }
   }
 
   /**
@@ -82,10 +424,16 @@ public function getSchema() {
 
       $table_mapping = $this->storage->getTableMapping();
       foreach ($table_mapping->getTableNames() as $table_name) {
-        // Add the schema from field definitions.
+        if (!isset($schema[$table_name])) {
+          $schema[$table_name] = array();
+        }
         foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
-          $column_names = $table_mapping->getColumnNames($field_name);
-          $this->addFieldSchema($schema[$table_name], $field_name, $column_names);
+          // Add the schema for base field definitions.
+          if ($table_mapping->allowsSharedTableStorage($this->fieldStorageDefinitions[$field_name])) {
+            $column_names = $table_mapping->getColumnNames($field_name);
+            $storage_definition = $this->fieldStorageDefinitions[$field_name];
+            $schema[$table_name] = array_merge_recursive($schema[$table_name], $this->getSharedTableFieldSchema($storage_definition, $column_names));
+          }
         }
 
         // Add the schema for extra fields.
@@ -132,16 +480,22 @@ protected function getTables() {
   /**
    * Returns the schema for a single field definition.
    *
-   * @param array $schema
-   *   The table schema to add the field schema to, passed by reference.
-   * @param string $field_name
-   *   The name of the field.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field whose schema has to be returned.
    * @param string[] $column_mapping
    *   A mapping of field column names to database column names.
    */
-  protected function addFieldSchema(array &$schema, $field_name, array $column_mapping) {
-    $field_schema = $this->fieldStorageDefinitions[$field_name]->getSchema();
-    $field_description = $this->fieldStorageDefinitions[$field_name]->getDescription();
+  protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, array $column_mapping) {
+    $schema = array();
+    $field_schema = $storage_definition->getSchema();
+
+    // Check that the schema does not include forbidden column names.
+    if (array_intersect(array_keys($field_schema['columns']), $this->storage->getTableMapping()->getReservedColumns())) {
+      throw new FieldException('Illegal field type columns.');
+    }
+
+    $field_name = $storage_definition->getName();
+    $field_description = $storage_definition->getDescription();
 
     foreach ($column_mapping as $field_column_name => $schema_field_name) {
       $column_schema = $field_schema['columns'][$field_column_name];
@@ -166,19 +520,18 @@ protected function addFieldSchema(array &$schema, $field_name, array $column_map
     }
 
     if (!empty($field_schema['indexes'])) {
-      $indexes = $this->getFieldIndexes($field_name, $field_schema, $column_mapping);
-      $schema['indexes'] = array_merge($schema['indexes'], $indexes);
+      $schema['indexes'] = $this->getFieldIndexes($field_name, $field_schema, $column_mapping);
     }
 
     if (!empty($field_schema['unique keys'])) {
-      $unique_keys = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping);
-      $schema['unique keys'] = array_merge($schema['unique keys'], $unique_keys);
+      $schema['unique keys'] = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping);
     }
 
     if (!empty($field_schema['foreign keys'])) {
-      $foreign_keys = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping);
-      $schema['foreign keys'] = array_merge($schema['foreign keys'], $foreign_keys);
+      $schema['foreign keys'] = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping);
     }
+
+    return $schema;
   }
 
   /**
@@ -309,6 +662,171 @@ protected function addDefaultLangcodeSchema(&$schema) {
     );
   }
 
+
+  /**
+   * Returns the SQL schema for a dedicated table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   *
+   * FIXME Do we still need these two? The deleted parameter is not used.
+   *
+   * @param array $schema
+   *   The field schema array. Mandatory for upgrades, omit otherwise.
+   * @param bool $deleted
+   *   (optional) Whether the schema of the table holding the values of a
+   *   deleted field should be returned.
+   *
+   * @return array
+   *   The same as a hook_schema() implementation for the data and the
+   *   revision tables.
+   *
+   * @see hook_schema()
+   */
+  protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, array $schema = NULL, $deleted = FALSE) {
+    $description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
+    $description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
+
+    $id_definition = $this->fieldStorageDefinitions[$this->entityType->getKey('id')];
+    if ($id_definition->getType() == 'integer') {
+      $id_schema = array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'description' => 'The entity id this data is attached to',
+      );
+    }
+    else {
+      $id_schema = array(
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => TRUE,
+        'description' => 'The entity id this data is attached to',
+      );
+    }
+
+    // Define the revision ID schema, default to integer if there is no revision
+    // ID.
+    // @todo Revisit this code: the revision id should match the entity id type
+    //   if revisions are not supported.
+    $revision_id_definition = $this->entityType->isRevisionable() ? $this->fieldStorageDefinitions[$this->entityType->getKey('revision')] : NULL;
+    if (!$revision_id_definition || $revision_id_definition->getType() == 'integer') {
+      $revision_id_schema = array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => FALSE,
+        'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned',
+      );
+    }
+    else {
+      $revision_id_schema = array(
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => FALSE,
+        'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned',
+      );
+    }
+
+    $data_schema = array(
+      'description' => $description_current,
+      'fields' => array(
+        'bundle' => array(
+          'type' => 'varchar',
+          'length' => 128,
+          'not null' => TRUE,
+          'default' => '',
+          'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance',
+        ),
+        'deleted' => array(
+          'type' => 'int',
+          'size' => 'tiny',
+          'not null' => TRUE,
+          'default' => 0,
+          'description' => 'A boolean indicating whether this data item has been deleted'
+        ),
+        'entity_id' => $id_schema,
+        'revision_id' => $revision_id_schema,
+        'langcode' => array(
+          'type' => 'varchar',
+          'length' => 32,
+          'not null' => TRUE,
+          'default' => '',
+          'description' => 'The language code for this data item.',
+        ),
+        'delta' => array(
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'description' => 'The sequence number for this data item, used for multi-value fields',
+        ),
+      ),
+      'primary key' => array('entity_id', 'deleted', 'delta', 'langcode'),
+      'indexes' => array(
+        'bundle' => array('bundle'),
+        'deleted' => array('deleted'),
+        'entity_id' => array('entity_id'),
+        'revision_id' => array('revision_id'),
+        'langcode' => array('langcode'),
+      ),
+    );
+
+    if (!$schema) {
+      $schema = $storage_definition->getSchema();
+    }
+
+    // Check that the schema does not include forbidden column names.
+    $table_mapping = $this->storage->getTableMapping();
+    if (array_intersect(array_keys($schema['columns']), $table_mapping->getReservedColumns())) {
+      throw new FieldException(format_string('Illegal field type @field_type on @field_name.', array('@field_type' => $this->type, '@field_name' => $this->name)));
+    }
+
+    // Add field columns.
+    foreach ($schema['columns'] as $column_name => $attributes) {
+      $real_name = $table_mapping->getFieldColumnName($storage_definition, $column_name);
+      $data_schema['fields'][$real_name] = $attributes;
+    }
+
+    // Add indexes.
+    foreach ($schema['indexes'] as $index_name => $columns) {
+      $real_name = $this->getFieldIndexName($storage_definition, $index_name);
+      foreach ($columns as $column_name) {
+        // Indexes can be specified as either a column name or an array with
+        // column name and length. Allow for either case.
+        if (is_array($column_name)) {
+          $data_schema['indexes'][$real_name][] = array(
+            $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
+            $column_name[1],
+          );
+        }
+        else {
+          $data_schema['indexes'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
+        }
+      }
+    }
+
+    // Add foreign keys.
+    foreach ($schema['foreign keys'] as $specifier => $specification) {
+      $real_name = $this->getFieldIndexName($storage_definition, $specifier);
+      $data_schema['foreign keys'][$real_name]['table'] = $specification['table'];
+      foreach ($specification['columns'] as $column_name => $referenced) {
+        $sql_storage_column = $table_mapping->getFieldColumnName($storage_definition, $column_name);
+        $data_schema['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced;
+      }
+    }
+
+    // Construct the revision table.
+    $revision_schema = $data_schema;
+    $revision_schema['description'] = $description_revision;
+    $revision_schema['primary key'] = array('entity_id', 'revision_id', 'deleted', 'delta', 'langcode');
+    $revision_schema['fields']['revision_id']['not null'] = TRUE;
+    $revision_schema['fields']['revision_id']['description'] = 'The entity revision id this data is attached to';
+
+    return array(
+      $table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema,
+      $table_mapping->getDedicatedRevisionTableName($storage_definition) => $revision_schema,
+    );
+  }
+
   /**
    * Initializes common information for a base table.
    *
@@ -341,6 +859,26 @@ protected function initializeBaseTable() {
   }
 
   /**
+   * Generates an index name for a field data table.
+   *
+   * @private Calling this function circumvents the entity system and is
+   * strongly discouraged. This function is not considered part of the public
+   * API and modules relying on it might break even in minor releases.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   * @param string $index
+   *   The name of the index.
+   *
+   * @return string
+   *   A string containing a generated index name for a field data table that is
+   *   unique among all other fields.
+   */
+  protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
+    return $storage_definition->getName() . '_' . $index;
+  }
+
+  /**
    * Initializes common information for a revision table.
    *
    * @return array
diff --git a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php
index a38b82c..c7fe82d 100644
--- a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php
+++ b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php
@@ -7,8 +7,48 @@
 
 namespace Drupal\Core\Entity\Schema;
 
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
 /**
  * Defines an interface for handling the storage schema of entities.
  */
 interface EntitySchemaHandlerInterface extends EntitySchemaProviderInterface {
+
+  /**
+   * Creates the storage schema for the given field.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being created.
+   */
+  public function createFieldSchema(FieldStorageDefinitionInterface $storage_definition);
+
+  /**
+   * Marks the storage schema for the given field as deleted.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being deleted.
+   */
+  public function markFieldSchemaAsDeleted(FieldStorageDefinitionInterface $storage_definition);
+
+  /**
+   * Deletes the storage schema for the given field.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being deleted.
+   */
+  public function deleteFieldSchema(FieldStorageDefinitionInterface $storage_definition);
+
+  /**
+   * Updates the storage schema for the given field.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being updated.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original storage definition; i.e., the definition before the update.
+   *
+   * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
+   *   Thrown when the update to the field is forbidden.
+   */
+  public function updateFieldSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original);
+
 }
diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
index e872d57..b0a5522 100644
--- a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
+++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
@@ -7,10 +7,12 @@
 
 namespace Drupal\Core\Entity\Sql;
 
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
 /**
  * Defines a default table mapping class.
  */
-class DefaultTableMapping implements TableMappingInterface {
+class DefaultTableMapping implements DefaultTableMappingInterface {
 
   /**
    * A list of field storage definitions that are available for this mapping.
@@ -20,6 +22,13 @@ class DefaultTableMapping implements TableMappingInterface {
   protected $fieldStorageDefinitions = array();
 
   /**
+   * A list of base field definitions that are available for this mapping.
+   *
+   * @var \Drupal\Core\Field\FieldDefinitionInterface[]
+   */
+  protected $baseFieldDefinitions = array();
+
+  /**
    * A list of field names per table.
    *
    * This corresponds to the return value of
@@ -76,9 +85,13 @@ class DefaultTableMapping implements TableMappingInterface {
    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
    *   A list of field storage definitions that should be available for the
    *   field columns of this table mapping.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface[] $base_field_definitions
+   *   A list of base field definitions that should be available for the field
+   *   columns of this table mapping.
    */
-  public function __construct(array $storage_definitions) {
+  public function __construct(array $storage_definitions, array $base_field_definitions) {
     $this->fieldStorageDefinitions = $storage_definitions;
+    $this->baseFieldDefinitions = $base_field_definitions;
   }
 
   /**
@@ -99,7 +112,14 @@ public function getAllColumns($table_name) {
         $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], array_values($this->getColumnNames($field_name)));
       }
 
-      $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], $this->getExtraColumns($table_name));
+      // There is just one field for each dedicated storage table, thus
+      // $field_name can only refer to it.
+      if (isset($field_name) && $this->requiresDedicatedTableStorage($this->fieldStorageDefinitions[$field_name])) {
+        $this->allColumns[$table_name] = array_merge($this->getExtraColumns($table_name), $this->allColumns[$table_name]);
+      }
+      else {
+        $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], $this->getExtraColumns($table_name));
+      }
     }
     return $this->allColumns[$table_name];
   }
@@ -177,4 +197,105 @@ public function setExtraColumns($table_name, array $column_names) {
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
+     return !$storage_definition->hasCustomStorage() && isset($this->baseFieldDefinitions[$storage_definition->getName()]) && !$storage_definition->isMultiple();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  function requiresDedicatedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
+    return !$storage_definition->hasCustomStorage() && (!isset($this->baseFieldDefinitions[$storage_definition->getName()]) || $storage_definition->isMultiple());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  function getDedicatedTableNames() {
+    // TODO
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getReservedColumns() {
+    return array('deleted');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDedicatedDataTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) {
+    if ($is_deleted) {
+      // When a field is a deleted, the table is renamed to
+      // {field_deleted_data_FIELD_UUID}. To make sure we don't end up with
+      // table names longer than 64 characters, we hash the unique storage
+      // identifier and return the first 10 characters so we end up with a short
+      // unique ID.
+      return "field_deleted_data_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
+    }
+    else {
+      return $this->generateFieldTableName($storage_definition, FALSE);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDedicatedRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) {
+    if ($is_deleted) {
+      // When a field is a deleted, the table is renamed to
+      // {field_deleted_revision_FIELD_UUID}. To make sure we don't end up with
+      // table names longer than 64 characters, we hash the unique storage
+      // identifier and return the first 10 characters so we end up with a short
+      // unique ID.
+      return "field_deleted_revision_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
+    }
+    else {
+      return $this->generateFieldTableName($storage_definition, TRUE);
+    }
+  }
+
+  /**
+   * Generates a safe and unambiguous field table name.
+   *
+   * The method accounts for a maximum table name length of 64 characters, and
+   * takes care of disambiguation.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   * @param bool $revision
+   *   TRUE for revision table, FALSE otherwise.
+   *
+   * @return string
+   *   The final table name.
+   */
+  protected function generateFieldTableName(FieldStorageDefinitionInterface $storage_definition, $revision) {
+    $separator = $revision ? '_revision__' : '__';
+    $table_name = $storage_definition->getTargetEntityTypeId() . $separator .  $storage_definition->getName();
+    // Limit the string to 48 characters, keeping a 16 characters margin for db
+    // prefixes.
+    if (strlen($table_name) > 48) {
+      // Use a shorter separator, a truncated entity_type, and a hash of the
+      // field UUID.
+      $separator = $revision ? '_r__' : '__';
+      // Truncate to the same length for the current and revision tables.
+      $entity_type = substr($storage_definition->getTargetEntityTypeId(), 0, 34);
+      $field_hash = substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
+      $table_name = $entity_type . $separator . $field_hash;
+    }
+    return $table_name;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldColumnName(FieldStorageDefinitionInterface $storage_definition, $column) {
+    return in_array($column, $this->getReservedColumns()) ? $column : $storage_definition->getName() . '_' . $column;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMappingInterface.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMappingInterface.php
new file mode 100644
index 0000000..a9e0ebf
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMappingInterface.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Sql\DefaultTableMappingInterface.
+ */
+
+namespace Drupal\Core\Entity\Sql;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
+/**
+ * Provides an common interface for mapping field columns to SQL tables.
+ */
+interface DefaultTableMappingInterface extends TableMappingInterface {
+
+  /**
+   * Returns a list of dedicated table names for this mapping.
+   *
+   * TODO Do we really need this or should we return everything in
+   *   TableMappingInterface::getTableNames()?
+   *
+   * @return string[]
+   *   An array of table names.
+   */
+  function getDedicatedTableNames();
+
+  /**
+   * Checks whether the given field can be stored in a shared table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   *
+   * @return bool
+   *   TRUE if the field can be stored in a dedicated table, FALSE otherwise.
+   */
+  function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition);
+
+  /**
+   * Checks whether the given field can be stored in a shared table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   *
+   * @return bool
+   *   TRUE if the field can be stored in a dedicated table, FALSE otherwise.
+   */
+  function requiresDedicatedTableStorage(FieldStorageDefinitionInterface $storage_definition);
+
+  /**
+   * A list of columns that can not be used as field type columns.
+   *
+   * @return array
+   */
+  public function getReservedColumns();
+
+  /**
+   * Generates a table name for a field data table.
+   *
+   * @private Calling this function circumvents the entity system and is
+   * strongly discouraged. This function is not considered part of the public
+   * API and modules relying on it might break even in minor releases. Only
+   * call this function to write a query that \Drupal::entityQuery() does not
+   * support. Always call entity_load() before using the data found in the
+   * table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   * @param bool $is_deleted
+   *   (optional) Whether the table name holding the values of a deleted field
+   *   should be returned.
+   *
+   * @return string
+   *   A string containing the generated name for the database table.
+   */
+  public function getDedicatedDataTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE);
+
+  /**
+   * Generates a table name for a field revision archive table.
+   *
+   * @private Calling this function circumvents the entity system and is
+   * strongly discouraged. This function is not considered part of the public
+   * API and modules relying on it might break even in minor releases. Only
+   * call this function to write a query that \Drupal::entityQuery() does not
+   * support. Always call entity_load() before using the data found in the
+   * table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   * @param bool $is_deleted
+   *   (optional) Whether the table name holding the values of a deleted field
+   *   should be returned.
+   *
+   * @return string
+   *   A string containing the generated name for the database table.
+   */
+  public function getDedicatedRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE);
+
+
+  /**
+   * Generates a column name for a field data table.
+   *
+   * @private Calling this function circumvents the entity system and is
+   * strongly discouraged. This function is not considered part of the public
+   * API and modules relying on it might break even in minor releases. Only
+   * call this function to write a query that \Drupal::entityQuery() does not
+   * support. Always call entity_load() before using the data found in the
+   * table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   * @param string $column
+   *   The name of the column.
+   *
+   * @return string
+   *   A string containing a generated column name for a field data table that is
+   *   unique among all other fields.
+   */
+  public function getFieldColumnName(FieldStorageDefinitionInterface $storage_definition, $column);
+
+}
diff --git a/core/lib/Drupal/Core/Field/FieldDefinition.php b/core/lib/Drupal/Core/Field/FieldDefinition.php
index a0576f6..d2aa2ad 100644
--- a/core/lib/Drupal/Core/Field/FieldDefinition.php
+++ b/core/lib/Drupal/Core/Field/FieldDefinition.php
@@ -509,8 +509,7 @@ public function getTargetEntityTypeId() {
    * @param string $entity_type_id
    *   The name of the target entity type to set.
    *
-   * @return static
-   *   The object itself for chaining.
+   * @return $this
    */
   public function setTargetEntityTypeId($entity_type_id) {
     $this->definition['entity_type'] = $entity_type_id;
@@ -520,6 +519,26 @@ public function setTargetEntityTypeId($entity_type_id) {
   /**
    * {@inheritdoc}
    */
+  public function getBundle() {
+    return isset($this->definition['bundle']) ? $this->definition['bundle'] : NULL;
+  }
+
+  /**
+   * Sets the bundle this field is defined for.
+   *
+   * @param string|null $bundle
+   *   The bundle, or NULL if the field is not bundle-specific.
+   *
+   * @return $this
+   */
+  public function setBundle($bundle) {
+    $this->definition['bundle'] = $bundle;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getSchema() {
     if (!isset($this->schema)) {
       // Get the schema from the field item class.
@@ -534,11 +553,6 @@ public function getSchema() {
         'foreign keys' => array(),
       );
 
-      // Check that the schema does not include forbidden column names.
-      if (array_intersect(array_keys($schema['columns']), static::getReservedColumns())) {
-        throw new FieldException('Illegal field type columns.');
-      }
-
       // Merge custom indexes with those specified by the field type. Custom
       // indexes prevail.
       $schema['indexes'] = $this->indexes + $schema['indexes'];
@@ -564,19 +578,10 @@ public function getColumns() {
   }
 
   /**
-   * A list of columns that can not be used as field type columns.
-   *
-   * @return array
-   */
-  public static function getReservedColumns() {
-    return array('deleted');
-  }
-
-  /**
    * {@inheritdoc}
    */
   public function hasCustomStorage() {
-    return !empty($this->definition['custom_storage']);
+    return !empty($this->definition['custom_storage']) || $this->isComputed();
   }
 
   /**
@@ -587,8 +592,14 @@ public function hasCustomStorage() {
    *   TRUE otherwise.
    *
    * @return $this
+   *
+   * @throws \LogicException
+   *   Thrown if custom storage is to be set to FALSE for a computed field.
    */
   public function setCustomStorage($custom_storage) {
+    if (!$custom_storage && $this->isComputed()) {
+      throw new \LogicException("Entity storage cannot store a computed field.");
+    }
     $this->definition['custom_storage'] = $custom_storage;
     return $this;
   }
@@ -600,4 +611,11 @@ public function getFieldStorageDefinition() {
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getUniqueStorageIdentifier() {
+    return $this->getTargetEntityTypeId() . '-' . $this->getName();
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php b/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php
index 2933656..be7f0fe 100644
--- a/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php
+++ b/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php
@@ -76,6 +76,15 @@ public function getName();
   public function getType();
 
   /**
+   * Gets the bundle the field is defined for.
+   *
+   * @return string|null
+   *   The bundle the field is defined for, or NULL if it is a base field; i.e.,
+   *   it is not bundle-specific.
+   */
+  public function getBundle();
+
+  /**
    * Returns whether the display for the field can be configured.
    *
    * @param string $display_context
diff --git a/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php b/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php
index 161b0b3..10f090e 100644
--- a/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php
+++ b/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php
@@ -297,4 +297,11 @@ public function getProvider();
    */
   public function hasCustomStorage();
 
+  /**
+   * Returns a unique identifier for the field.
+   *
+   * @return string
+   */
+  public function getUniqueStorageIdentifier();
+
 }
diff --git a/core/modules/comment/src/Tests/CommentFieldsTest.php b/core/modules/comment/src/Tests/CommentFieldsTest.php
index 5dd3bb7..5bb4380 100644
--- a/core/modules/comment/src/Tests/CommentFieldsTest.php
+++ b/core/modules/comment/src/Tests/CommentFieldsTest.php
@@ -85,9 +85,6 @@ function testCommentInstallAfterContentModule() {
     // Purge field data now to allow comment module to be uninstalled once the
     // field has been deleted.
     field_purge_batch(10);
-    // Call again as field_purge_batch() won't remove both the instances and
-    // field in a single pass.
-    field_purge_batch(10);
 
     // Disable the comment module.
     $edit = array();
diff --git a/core/modules/contact/src/Tests/Views/ContactFieldsTest.php b/core/modules/contact/src/Tests/Views/ContactFieldsTest.php
index 4308ca7..5e456a4 100644
--- a/core/modules/contact/src/Tests/Views/ContactFieldsTest.php
+++ b/core/modules/contact/src/Tests/Views/ContactFieldsTest.php
@@ -66,7 +66,7 @@ protected function setUp() {
   public function testViewsData() {
     // Test that the field is not exposed to views, since contact_message
     // entities have no storage.
-    $table_name = ContentEntityDatabaseStorage::_fieldTableName($this->field);
+    $table_name = 'contact_message__' .  $this->field->getName();
     $data = $this->container->get('views.views_data')->get($table_name);
     $this->assertFalse($data, 'The field is not exposed to Views.');
   }
diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php
index e8fb142..f364c51 100644
--- a/core/modules/field/field.api.php
+++ b/core/modules/field/field.api.php
@@ -6,7 +6,6 @@
  */
 
 use Drupal\Component\Utility\NestedArray;
-use Drupal\field\FieldConfigUpdateForbiddenException;
 
 /**
  * @defgroup field_types Field Types API
@@ -248,7 +247,7 @@ function hook_field_info_max_weight($entity_type, $bundle, $context, $context_mo
  * that cannot be updated.
  *
  * To forbid the update from occurring, throw a
- * Drupal\field\FieldConfigUpdateForbiddenException.
+ * \Drupal\Core\Entity\Exception\StorageDefinitionUpdateForbiddenException.
  *
  * @param \Drupal\field\FieldConfigInterface $field
  *   The field as it will be post-update.
@@ -270,7 +269,7 @@ function hook_field_config_update_forbid(\Drupal\field\FieldConfigInterface $fie
       ->range(0, 1)
       ->execute();
     if ($found) {
-      throw new FieldConfigUpdateForbiddenException("Cannot update a list field not to include keys with existing data");
+      throw new \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException("Cannot update a list field not to include keys with existing data");
     }
   }
 }
diff --git a/core/modules/field/field.module b/core/modules/field/field.module
index 1cd2972..3874c4e 100644
--- a/core/modules/field/field.module
+++ b/core/modules/field/field.module
@@ -198,20 +198,27 @@ function field_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundl
   }
 }
 
-
 /**
  * Implements hook_entity_bundle_rename().
  */
 function field_entity_bundle_rename($entity_type, $bundle_old, $bundle_new) {
-  $instances = entity_load_multiple_by_properties('field_instance_config', array('entity_type' => $entity_type, 'bundle' => $bundle_old));
+  $instances = entity_load_multiple_by_properties('field_instance_config', array('entity_type' => $entity_type, 'bundle' => $bundle_old, 'include_deleted' => TRUE));
   foreach ($instances as $instance) {
-    if ($instance->entity_type == $entity_type && $instance->bundle == $bundle_old) {
-      $id_new = $instance->entity_type . '.' . $bundle_new . '.' . $instance->field_name;
-      $instance->set('id', $id_new);
-      $instance->bundle = $bundle_new;
+    $id_new = $instance->entity_type . '.' . $bundle_new . '.' . $instance->field_name;
+    $instance->set('id', $id_new);
+    $instance->bundle = $bundle_new;
+    // Save non-deleted instances.
+    if (!$instance->isDeleted()) {
       $instance->allowBundleRename();
       $instance->save();
     }
+    // Update deleted instances directly in the state storage.
+    else {
+      $state = \Drupal::state();
+      $deleted_instances = $state->get('field.instance.deleted') ?: array();
+      $deleted_instances[$instance->uuid] = $instance->toArray();
+      $state->set('field.instance.deleted', $deleted_instances);
+    }
   }
 }
 
diff --git a/core/modules/field/field.purge.inc b/core/modules/field/field.purge.inc
index 4471970..4a289cf 100644
--- a/core/modules/field/field.purge.inc
+++ b/core/modules/field/field.purge.inc
@@ -80,7 +80,6 @@ function field_purge_batch($batch_size, $field_uuid = NULL) {
   else {
     $instances = entity_load_multiple_by_properties('field_instance_config', array('deleted' => TRUE, 'include_deleted' => TRUE));
   }
-  $factory = \Drupal::service('entity.query');
   $info = \Drupal::entityManager()->getDefinitions();
   foreach ($instances as $instance) {
     $entity_type = $instance->entity_type;
@@ -92,36 +91,16 @@ function field_purge_batch($batch_size, $field_uuid = NULL) {
       continue;
     }
 
-    $ids = (object) array(
-      'entity_type' => $entity_type,
-      'bundle' => $instance->bundle,
-    );
-    // Retrieve some entities.
-    $query = $factory->get($entity_type)
-      ->condition('id:' . $instance->getFieldStorageDefinition()->uuid() . '.deleted', 1)
-      ->range(0, $batch_size);
-    // If there's no bundle key, all results will have the same bundle.
-    if ($bundle_key = $info[$entity_type]->getKey('bundle')) {
-      $query->condition($bundle_key, $ids->bundle);
-    }
-    $results = $query->execute();
-    if ($results) {
-      foreach ($results as $revision_id => $entity_id) {
-        $ids->revision_id = $revision_id;
-        $ids->entity_id = $entity_id;
-        $entity = _field_create_entity_from_ids($ids);
-        \Drupal::entityManager()->getStorage($entity_type)->onFieldItemsPurge($entity, $instance);
-        $batch_size--;
-      }
-      // Only delete up to the maximum number of records.
-      if ($batch_size == 0) {
-        break;
-      }
-    }
-    else {
+    $count_purged = \Drupal::entityManager()->getStorage($entity_type)->purgeFieldData($instance, $batch_size);
+    if ($count_purged < $batch_size || $count_purged == 0) {
       // No field data remains for the instance, so we can remove it.
       field_purge_instance($instance);
     }
+    $batch_size -= $count_purged;
+    // Only delete up to the maximum number of records.
+    if ($batch_size == 0) {
+      break;
+    }
   }
 
   // Retrieve all deleted fields. Any that have no instances can be purged.
@@ -187,7 +166,7 @@ function field_purge_field($field) {
   $state->set('field.field.deleted', $deleted_fields);
 
   // Notify the storage layer.
-  \Drupal::entityManager()->getStorage($field->entity_type)->onFieldPurge($field);
+  \Drupal::entityManager()->getStorage($field->entity_type)->finalizePurge($field);
 
   // Invoke external hooks after the cache is cleared for API consistency.
   \Drupal::moduleHandler()->invokeAll('field_purge_field', array($field));
diff --git a/core/modules/field/field.views.inc b/core/modules/field/field.views.inc
index f74e97c..dd52cf3 100644
--- a/core/modules/field/field.views.inc
+++ b/core/modules/field/field.views.inc
@@ -22,7 +22,7 @@ function field_views_data() {
   $module_handler = \Drupal::moduleHandler();
 
   foreach (\Drupal::entityManager()->getStorage('field_config')->loadMultiple() as $field) {
-    if (_field_views_is_sql_entity_type($field)) {
+    if (_field_views_get_entity_type_storage($field)) {
       $result = (array) $module_handler->invoke($field->module, 'field_views_data', array($field));
       if (empty($result)) {
         $result = field_views_field_default_views_data($field);
@@ -48,7 +48,7 @@ function field_views_data() {
  */
 function field_views_data_alter(&$data) {
   foreach (\Drupal::entityManager()->getStorage('field_config')->loadMultiple() as $field) {
-    if (_field_views_is_sql_entity_type($field)) {
+    if (_field_views_get_entity_type_storage($field)) {
       $function = $field->module . '_field_views_data_views_data_alter';
       if (function_exists($function)) {
         $function($data, $field);
@@ -63,12 +63,17 @@ function field_views_data_alter(&$data) {
  * @param \Drupal\field\FieldConfigInterface $field
  *   The field definition.
  *
- * @return bool
- *   True if the entity type uses ContentEntityDatabaseStorage.
+ * @return \Drupal\Core\Entity\ContentEntityDatabaseStorage
+ *   Returns the entity type storage if supported.
  */
-function _field_views_is_sql_entity_type(FieldConfigInterface $field) {
+function _field_views_get_entity_type_storage(FieldConfigInterface $field) {
+  $result = FALSE;
   $entity_manager = \Drupal::entityManager();
-  return $entity_manager->hasDefinition($field->entity_type) && $entity_manager->getStorage($field->entity_type) instanceof ContentEntityDatabaseStorage;
+  if ($entity_manager->hasDefinition($field->entity_type)) {
+    $storage = $entity_manager->getStorage($field->entity_type);
+    $result = $storage instanceof ContentEntityDatabaseStorage ? $storage : FALSE;
+  }
+  return $result;
 }
 
 /**
@@ -120,6 +125,11 @@ function field_views_field_default_views_data(FieldConfigInterface $field) {
   if (!$field->getBundles()) {
     return $data;
   }
+  // Check whether the entity type storage is supported.
+  $storage = _field_views_get_entity_type_storage($field);
+  if (!$storage) {
+    return $data;
+  }
 
   $field_name = $field->getName();
   $field_columns = $field->getColumns();
@@ -139,15 +149,18 @@ function field_views_field_default_views_data(FieldConfigInterface $field) {
   }
 
   // Description of the field tables.
+  // @todo Generalize this code to make it work with any table layout. See
+  //   https://drupal.org/node/2079019.
+  $table_mapping = $storage->getTableMapping();
   $field_tables = array(
     EntityStorageInterface::FIELD_LOAD_CURRENT => array(
-      'table' => ContentEntityDatabaseStorage::_fieldTableName($field),
+      'table' => $table_mapping->getDedicatedDataTableName($field),
       'alias' => "{$entity_type_id}__{$field_name}",
     ),
   );
   if ($supports_revisions) {
     $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION] = array(
-      'table' => ContentEntityDatabaseStorage::_fieldRevisionTableName($field),
+      'table' => $table_mapping->getDedicatedRevisionTableName($field),
       'alias' => "{$entity_type_id}_revision__{$field_name}",
     );
   }
@@ -178,7 +191,7 @@ function field_views_field_default_views_data(FieldConfigInterface $field) {
   // Build the list of additional fields to add to queries.
   $add_fields = array('delta', 'langcode', 'bundle');
   foreach (array_keys($field_columns) as $column) {
-    $add_fields[] = ContentEntityDatabaseStorage::_fieldColumnName($field, $column);
+    $add_fields[] = $table_mapping->getFieldColumnName($field, $column);
   }
   // Determine the label to use for the field. We don't have a label available
   // at the field level, so we just go through all instances and take the one
@@ -302,11 +315,10 @@ function field_views_field_default_views_data(FieldConfigInterface $field) {
       else {
         $group = t('@group (historical data)', array('@group' => $group_name));
       }
-      $column_real_name = ContentEntityDatabaseStorage::_fieldColumnName($field, $column);
+      $column_real_name = $table_mapping->getFieldColumnName($field, $column);
 
       // Load all the fields from the table by default.
-      $field_sql_schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field);
-      $additional_fields = array_keys($field_sql_schema[$table]['fields']);
+      $additional_fields = $table_mapping->getAllColumns($table);
 
       $data[$table_alias][$column_real_name] = array(
         'group' => $group,
diff --git a/core/modules/field/src/ConfigImporterFieldPurger.php b/core/modules/field/src/ConfigImporterFieldPurger.php
index db3d8b9..a33df39 100644
--- a/core/modules/field/src/ConfigImporterFieldPurger.php
+++ b/core/modules/field/src/ConfigImporterFieldPurger.php
@@ -75,7 +75,8 @@ protected static function initializeSandbox(array &$context, ConfigImporter $con
     $context['sandbox']['field']['steps_to_delete'] = 0;
     $fields = static::getFieldsToPurge($context['sandbox']['field']['extensions'], $config_importer->getUnprocessedConfiguration('delete'));
     foreach ($fields as $field) {
-      $row_count = $field->entityCount();
+      $row_count = \Drupal::entityManager()->getStorage($field->getTargetEntityTypeId())
+        ->countFieldData($field);
       if ($row_count > 0) {
         // The number of steps to delete each field is determined by the
         // purge_batch_size setting. For example if the field has 9 rows and the
@@ -84,8 +85,8 @@ protected static function initializeSandbox(array &$context, ConfigImporter $con
         $context['sandbox']['field']['steps_to_delete'] += $how_many_steps;
       }
     }
-    // Each field needs one last field_purge_batch() call to remove the last
-    // instance and the field itself.
+    // Each field possibly needs one last field_purge_batch() call to remove the
+    // last instance and the field itself.
     $context['sandbox']['field']['steps_to_delete'] += count($fields);
 
     $context['sandbox']['field']['current_progress'] = 0;
diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php
index 8235487..7f80220 100644
--- a/core/modules/field/src/Entity/FieldConfig.php
+++ b/core/modules/field/src/Entity/FieldConfig.php
@@ -293,7 +293,7 @@ protected function preSaveNew(EntityStorageInterface $storage) {
      $this->settings += $field_type_manager->getDefaultSettings($this->type);
 
     // Notify the entity storage.
-    $entity_manager->getStorage($this->entity_type)->onFieldCreate($this);
+    $entity_manager->getStorage($this->entity_type)->onFieldStorageDefinitionCreate($this);
   }
 
   /**
@@ -339,7 +339,7 @@ protected function preSaveUpdated(EntityStorageInterface $storage) {
     // Notify the storage. The controller can reject the definition
     // update as invalid by raising an exception, which stops execution before
     // the definition is written to config.
-    $entity_manager->getStorage($this->entity_type)->onFieldUpdate($this);
+    $entity_manager->getStorage($this->entity_type)->onFieldStorageDefinitionUpdate($this, $this->original);
   }
 
   /**
@@ -408,7 +408,7 @@ public static function postDelete(EntityStorageInterface $storage, array $fields
     // Notify the storage.
     foreach ($fields as $field) {
       if (!$field->deleted) {
-        \Drupal::entityManager()->getStorage($field->entity_type)->onFieldDelete($field);
+        \Drupal::entityManager()->getStorage($field->entity_type)->onFieldStorageDefinitionDelete($field);
         $field->deleted = TRUE;
       }
     }
@@ -432,11 +432,6 @@ public function getSchema() {
         'foreign keys' => array(),
       );
 
-      // Check that the schema does not include forbidden column names.
-      if (array_intersect(array_keys($schema['columns']), static::getReservedColumns())) {
-        throw new FieldException(String::format('Illegal field type @field_type on @field_name.', array('@field_type' => $this->type, '@field_name' => $this->name)));
-      }
-
       // Merge custom indexes with those specified by the field type. Custom
       // indexes prevail.
       $schema['indexes'] = $this->indexes + $schema['indexes'];
@@ -616,74 +611,13 @@ public function isQueryable() {
   }
 
   /**
-   * A list of columns that can not be used as field type columns.
-   *
-   * @return array
-   */
-  public static function getReservedColumns() {
-    return array('deleted');
-  }
-
-  /**
    * Determines whether a field has any data.
    *
    * @return bool
    *   TRUE if the field has data for any entity; FALSE otherwise.
    */
   public function hasData() {
-    return $this->entityCount(TRUE);
-  }
-
-  /**
-   * Determines the number of entities that have field data.
-   *
-   * @param bool $as_bool
-   *   (Optional) Optimises query for hasData(). Defaults to FALSE.
-   *
-   * @return bool|int
-   *   The number of entities that have field data. If $as_bool parameter is
-   *   TRUE then the value will either be TRUE or FALSE.
-   */
-  public function entityCount($as_bool = FALSE) {
-    $count = 0;
-    $factory = \Drupal::service('entity.query');
-    $entity_type = \Drupal::entityManager()->getDefinition($this->entity_type);
-    // Entity Query throws an exception if there is no base table.
-    if ($entity_type->getBaseTable()) {
-      if ($this->deleted) {
-        $query = $factory->get($this->entity_type)
-          ->condition('id:' . $this->uuid() . '.deleted', 1);
-      }
-      elseif ($this->getBundles()) {
-        $storage_details = $this->getSchema();
-        $columns = array_keys($storage_details['columns']);
-        $query = $factory->get($this->entity_type);
-        $group = $query->orConditionGroup();
-        foreach ($columns as $column) {
-          $group->exists($this->name . '.' . $column);
-        }
-        $query = $query->condition($group);
-      }
-
-      if (isset($query)) {
-        $query
-          ->count()
-          ->accessCheck(FALSE);
-        // If we are performing the query just to check if the field has data
-        // limit the number of rows returned by the subquery.
-        if ($as_bool) {
-          $query->range(0, 1);
-        }
-        $count = $query->execute();
-      }
-    }
-
-    if ($as_bool) {
-      return (bool) $count;
-    }
-    else {
-      return (int) $count;
-    }
+    return \Drupal::entityManager()->getStorage($this->entity_type)->countFieldData($this, TRUE);
   }
 
   /**
@@ -763,6 +697,13 @@ public function getMainPropertyName() {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function getUniqueStorageIdentifier() {
+    return $this->uuid();
+  }
+
+  /**
    * Helper to retrieve the field item class.
    */
   protected function getFieldItemClass() {
diff --git a/core/modules/field/src/Entity/FieldInstanceConfig.php b/core/modules/field/src/Entity/FieldInstanceConfig.php
index f6d62f7..25fd4aa 100644
--- a/core/modules/field/src/Entity/FieldInstanceConfig.php
+++ b/core/modules/field/src/Entity/FieldInstanceConfig.php
@@ -336,7 +336,7 @@ public function preSave(EntityStorageInterface $storage) {
       // Set the default instance settings.
       $this->settings += $field_type_manager->getDefaultInstanceSettings($field->type);
       // Notify the entity storage.
-      $entity_manager->getStorage($this->entity_type)->onInstanceCreate($this);
+      $entity_manager->getStorage($this->entity_type)->onFieldDefinitionCreate($this);
     }
     else {
       // Some updates are always disallowed.
@@ -352,7 +352,7 @@ public function preSave(EntityStorageInterface $storage) {
       // Set the default instance settings.
       $this->settings += $field_type_manager->getDefaultInstanceSettings($field->type);
       // Notify the entity storage.
-      $entity_manager->getStorage($this->entity_type)->onInstanceUpdate($this);
+      $entity_manager->getStorage($this->entity_type)->onFieldDefinitionUpdate($this, $this->original);
     }
     if (!$this->isSyncing()) {
       // Ensure the correct dependencies are present.
@@ -423,7 +423,7 @@ public static function postDelete(EntityStorageInterface $storage, array $instan
     // Notify the entity storage.
     foreach ($instances as $instance) {
       if (!$instance->deleted) {
-        \Drupal::entityManager()->getStorage($instance->entity_type)->onInstanceDelete($instance);
+        \Drupal::entityManager()->getStorage($instance->entity_type)->onFieldDefinitionDelete($instance);
       }
     }
 
@@ -606,6 +606,13 @@ public function getDisplayOptions($display_context) {
   /**
    * {@inheritdoc}
    */
+  public function getBundle() {
+    return $this->bundle;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function allowBundleRename() {
     $this->bundle_rename_allowed = TRUE;
   }
diff --git a/core/modules/field/src/FieldConfigUpdateForbiddenException.php b/core/modules/field/src/FieldConfigUpdateForbiddenException.php
deleted file mode 100644
index 412e146..0000000
--- a/core/modules/field/src/FieldConfigUpdateForbiddenException.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\field\FieldConfigUpdateForbiddenException.
- */
-
-namespace Drupal\field;
-
-/**
- * Exception class thrown by hook_field_config_update_forbid().
- */
-class FieldConfigUpdateForbiddenException extends FieldException {}
diff --git a/core/modules/field/src/FieldInstanceConfigStorage.php b/core/modules/field/src/FieldInstanceConfigStorage.php
index 42b258e..742319d 100644
--- a/core/modules/field/src/FieldInstanceConfigStorage.php
+++ b/core/modules/field/src/FieldInstanceConfigStorage.php
@@ -161,7 +161,10 @@ public function loadByProperties(array $conditions = array()) {
         }
       }
 
-      $matching_instances[] = $instance;
+      // When returning deleted instances, key the results by UUID since they
+      // can include several instances with the same ID.
+      $key = $include_deleted ? $instance->uuid() : $instance->id();
+      $matching_instances[$key] = $instance;
     }
 
     return $matching_instances;
diff --git a/core/modules/field/src/Plugin/views/field/Field.php b/core/modules/field/src/Plugin/views/field/Field.php
index c678554..05c8342 100644
--- a/core/modules/field/src/Plugin/views/field/Field.php
+++ b/core/modules/field/src/Plugin/views/field/Field.php
@@ -349,9 +349,12 @@ public function clickSort($order) {
     }
 
     $this->ensureMyTable();
-    $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->definition['entity_type']);
+    $entity_type_id = $this->definition['entity_type'];
+    $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
     $field = $field_storage_definitions[$this->definition['field_name']];
-    $column = ContentEntityDatabaseStorage::_fieldColumnName($field, $this->options['click_sort_column']);
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+    $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping();
+    $column = $table_mapping->getFieldColumnName($field, $this->options['click_sort_column']);
     if (!isset($this->aliases[$column])) {
       // Column is not in query; add a sort on it (without adding the column).
       $this->aliases[$column] = $this->tableAlias . '.' . $column;
diff --git a/core/modules/field/src/Tests/BulkDeleteTest.php b/core/modules/field/src/Tests/BulkDeleteTest.php
index cb0e8f9..41985eb 100644
--- a/core/modules/field/src/Tests/BulkDeleteTest.php
+++ b/core/modules/field/src/Tests/BulkDeleteTest.php
@@ -182,15 +182,17 @@ function testDeleteFieldInstance() {
     // The instance still exists, deleted.
     $instances = entity_load_multiple_by_properties('field_instance_config', array('field_id' => $field->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE));
     $this->assertEqual(count($instances), 1, 'There is one deleted instance');
-    $instance = $instances[0];
+    $instance = $instances[$instance->uuid()];
     $this->assertEqual($instance->bundle, $bundle, 'The deleted instance is for the correct bundle');
 
     // Check that the actual stored content did not change during delete.
-    $schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field);
-    $table = ContentEntityDatabaseStorage::_fieldTableName($field);
-    $column = ContentEntityDatabaseStorage::_fieldColumnName($field, 'value');
+    $storage = \Drupal::entityManager()->getStorage($this->entity_type);
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+    $table_mapping = $storage->getTableMapping();
+    $table = $table_mapping->getDedicatedDataTableName($field);
+    $column = $table_mapping->getFieldColumnName($field, 'value');
     $result = db_select($table, 't')
-      ->fields('t', array_keys($schema[$table]['fields']))
+      ->fields('t')
       ->execute();
     foreach ($result as $row) {
       $this->assertEqual($this->entities[$row->entity_id]->{$field->name}->value, $row->$column);
@@ -306,9 +308,16 @@ function testPurgeField() {
     }
     $this->checkHooksInvocations($hooks, $actual_hooks);
 
+    // The instance still exists, deleted.
+    $instances = entity_load_multiple_by_properties('field_instance_config', array('uuid' => $instance->uuid(), 'include_deleted' => TRUE));
+    $this->assertTrue(isset($instances[$instance->uuid()]) && $instances[$instance->uuid()]->deleted, 'The instance exists and is deleted');
+
     // Purge again to purge the instance.
     field_purge_batch(0);
 
+    // The instance is gone.
+    $instances = entity_load_multiple_by_properties('field_instance_config', array('uuid' => $instance->uuid(), 'include_deleted' => TRUE));
+    $this->assertEqual(count($instances), 0, 'The instance is purged.');
     // The field still exists, not deleted.
     $fields = entity_load_multiple_by_properties('field_config', array('uuid' => $field->uuid(), 'include_deleted' => TRUE));
     $this->assertTrue(isset($fields[$field->uuid()]) && !$fields[$field->uuid()]->deleted, 'The field exists and is not deleted');
@@ -334,14 +343,18 @@ function testPurgeField() {
     }
     $this->checkHooksInvocations($hooks, $actual_hooks);
 
-    // The field still exists, deleted.
+    // The field and instance still exist, deleted.
+    $instances = entity_load_multiple_by_properties('field_instance_config', array('uuid' => $instance->uuid(), 'include_deleted' => TRUE));
+    $this->assertTrue(isset($instances[$instance->uuid()]) && $instances[$instance->uuid()]->deleted, 'The instance exists and is deleted');
     $fields = entity_load_multiple_by_properties('field_config', array('uuid' => $field->uuid(), 'include_deleted' => TRUE));
     $this->assertTrue(isset($fields[$field->uuid()]) && $fields[$field->uuid()]->deleted, 'The field exists and is deleted');
 
     // Purge again to purge the instance and the field.
     field_purge_batch(0);
 
-    // The field is gone.
+    // The field and instance are gone.
+    $instances = entity_load_multiple_by_properties('field_instance_config', array('uuid' => $instance->uuid(), 'include_deleted' => TRUE));
+    $this->assertEqual(count($instances), 0, 'The instance is purged.');
     $fields = entity_load_multiple_by_properties('field_config', array('uuid' => $field->uuid(), 'include_deleted' => TRUE));
     $this->assertEqual(count($fields), 0, 'The field is purged.');
   }
diff --git a/core/modules/field/src/Tests/CrudTest.php b/core/modules/field/src/Tests/CrudTest.php
index 782897c..6a16101 100644
--- a/core/modules/field/src/Tests/CrudTest.php
+++ b/core/modules/field/src/Tests/CrudTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\field\Tests;
 
 use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\FieldException;
 
@@ -440,7 +441,7 @@ function testUpdateFieldForbid() {
       $field->save();
       $this->pass(t("A changeable setting can be updated."));
     }
-    catch (FieldException $e) {
+    catch (FieldStorageDefinitionUpdateForbiddenException $e) {
       $this->fail(t("An unchangeable setting cannot be updated."));
     }
     $field->settings['unchangeable']++;
@@ -448,7 +449,7 @@ function testUpdateFieldForbid() {
       $field->save();
       $this->fail(t("An unchangeable setting can be updated."));
     }
-    catch (FieldException $e) {
+    catch (FieldStorageDefinitionUpdateForbiddenException $e) {
       $this->pass(t("An unchangeable setting cannot be updated."));
     }
   }
diff --git a/core/modules/field/src/Tests/FieldEntityCountTest.php b/core/modules/field/src/Tests/FieldDataCountTest.php
similarity index 63%
rename from core/modules/field/src/Tests/FieldEntityCountTest.php
rename to core/modules/field/src/Tests/FieldDataCountTest.php
index b55619f..78ce402 100644
--- a/core/modules/field/src/Tests/FieldEntityCountTest.php
+++ b/core/modules/field/src/Tests/FieldDataCountTest.php
@@ -2,7 +2,7 @@
 
 /**
  * @file
- * Contains \Drupal\field\Tests\FieldEntityCountTest.
+ * Contains \Drupal\field\Tests\FieldDataCountTest.
  */
 
 namespace Drupal\field\Tests;
@@ -10,22 +10,38 @@
 use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 
 /**
- * Tests entityCount() and hasData() methods on FieldConfig entity.
+ * Tests counting field data records.
  *
- * @see \Drupal\field\Entity\FieldConfig::entityCount()
+ * @see \Drupal\Core\Entity\FieldableEntityStorageInterface::countFieldData()
  * @see \Drupal\field\Entity\FieldConfig::hasData()
  */
-class FieldEntityCountTest extends FieldUnitTestBase {
+class FieldDataCountTest extends FieldUnitTestBase {
 
+  /**
+   * @var \Drupal\Core\Entity\FieldableEntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * {@inheritdoc}
+   */
   public static function getInfo() {
     return array(
-      'name' => 'Field config entityCount() and hasData() tests.',
-      'description' => 'Tests entityCount() and hasData() methods on FieldConfig entity.',
+      'name' => 'Field config hasData() tests.',
+      'description' => 'Tests counting field data records and the hasData() method on FieldConfig entity.',
       'group' => 'Field API',
     );
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->storage = \Drupal::entityManager()->getStorage('entity_test');
+  }
+
+  /**
    * Tests entityCount() and hadData() methods.
    */
   public function testEntityCountAndHasData() {
@@ -45,7 +61,7 @@ public function testEntityCountAndHasData() {
     ))->save();
 
     $this->assertIdentical($field->hasdata(), FALSE, 'There are no entities with field data.');
-    $this->assertIdentical($field->entityCount(), 0, 'There are 0 entities with field data.');
+    $this->assertIdentical($this->storage->countFieldData($field), 0, 'There are 0 entities with field data.');
 
     // Create 1 entity without the field.
     $entity = entity_create('entity_test');
@@ -53,7 +69,7 @@ public function testEntityCountAndHasData() {
     $entity->save();
 
     $this->assertIdentical($field->hasdata(), FALSE, 'There are no entities with field data.');
-    $this->assertIdentical($field->entityCount(), 0, 'There are 0 entities with field data.');
+    $this->assertIdentical($this->storage->countFieldData($field), 0, 'There are 0 entities with field data.');
 
     // Create 12 entities to ensure that the purging works as expected.
     for ($i=0; $i < 12; $i++) {
@@ -69,7 +85,8 @@ public function testEntityCountAndHasData() {
     $storage = \Drupal::entityManager()->getStorage('entity_test');
     if ($storage instanceof ContentEntityDatabaseStorage) {
       // Count the actual number of rows in the field table.
-      $field_table_name = $storage->_fieldTableName($field);
+      $table_mapping = $storage->getTableMapping();
+      $field_table_name = $table_mapping->getDedicatedDataTableName($field);
       $result = db_select($field_table_name, 't')
         ->fields('t')
         ->countQuery()
@@ -79,16 +96,16 @@ public function testEntityCountAndHasData() {
     }
 
     $this->assertIdentical($field->hasdata(), TRUE, 'There are entities with field data.');
-    $this->assertEqual($field->entityCount(), 12, 'There are 12 entities with field data.');
+    $this->assertEqual($this->storage->countFieldData($field), 12, 'There are 12 entities with field data.');
 
     // Ensure the methods work on deleted fields.
     $field->delete();
     $this->assertIdentical($field->hasdata(), TRUE, 'There are entities with deleted field data.');
-    $this->assertEqual($field->entityCount(), 12, 'There are 12 entities with deleted field data.');
+    $this->assertEqual($this->storage->countFieldData($field), 12, 'There are 12 entities with deleted field data.');
 
     field_purge_batch(6);
     $this->assertIdentical($field->hasdata(), TRUE, 'There are entities with deleted field data.');
-    $this->assertEqual($field->entityCount(), 6, 'There are 6 entities with deleted field data.');
+    $this->assertEqual($this->storage->countFieldData($field), 6, 'There are 6 entities with deleted field data.');
   }
 
 }
diff --git a/core/modules/field/src/Tests/Views/ApiDataTest.php b/core/modules/field/src/Tests/Views/ApiDataTest.php
index be0d496..3f05fec 100644
--- a/core/modules/field/src/Tests/Views/ApiDataTest.php
+++ b/core/modules/field/src/Tests/Views/ApiDataTest.php
@@ -62,8 +62,10 @@ function testViewsData() {
     // Check the table and the joins of the first field.
     // Attached to node only.
     $field = $this->fields[0];
-    $current_table = ContentEntityDatabaseStorage::_fieldTableName($field);
-    $revision_table = ContentEntityDatabaseStorage::_fieldRevisionTableName($field);
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+    $table_mapping = \Drupal::entityManager()->getStorage('node')->getTableMapping();
+    $current_table = $table_mapping->getDedicatedDataTableName($field);
+    $revision_table = $table_mapping->getDedicatedRevisionTableName($field);
     $data[$current_table] = $views_data->get($current_table);
     $data[$revision_table] = $views_data->get($revision_table);
 
diff --git a/core/modules/field/tests/modules/field_test/field_test.field.inc b/core/modules/field/tests/modules/field_test/field_test.field.inc
index e43a7e7..93e090b 100644
--- a/core/modules/field/tests/modules/field_test/field_test.field.inc
+++ b/core/modules/field/tests/modules/field_test/field_test.field.inc
@@ -10,7 +10,7 @@
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\field\FieldConfigInterface;
-use Drupal\field\FieldConfigUpdateForbiddenException;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 
 /**
  * Implements hook_field_widget_info_alter().
@@ -24,7 +24,7 @@ function field_test_field_widget_info_alter(&$info) {
  */
 function field_test_field_config_update_forbid(FieldConfigInterface $field, FieldConfigInterface $prior_field) {
   if ($field->getType() == 'test_field' && $field->getSetting('unchangeable') != $prior_field->getSetting('unchangeable')) {
-    throw new FieldConfigUpdateForbiddenException("field_test 'unchangeable' setting cannot be changed'");
+    throw new FieldStorageDefinitionUpdateForbiddenException("field_test 'unchangeable' setting cannot be changed'");
   }
 }
 
diff --git a/core/modules/file/file.views.inc b/core/modules/file/file.views.inc
index e1029d5..959e141 100644
--- a/core/modules/file/file.views.inc
+++ b/core/modules/file/file.views.inc
@@ -520,9 +520,12 @@ function file_field_views_data(FieldConfigInterface $field) {
  */
 function file_field_views_data_views_data_alter(array &$data, FieldConfigInterface $field) {
   $entity_type_id = $field->entity_type;
-  $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id);
+  $entity_manager = \Drupal::entityManager();
+  $entity_type = $entity_manager->getDefinition($entity_type_id);
   $field_name = $field->getName();
   $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
+  /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+  $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping();
 
   list($label) = field_views_field_label($entity_type_id, $field_name);
 
@@ -532,7 +535,7 @@ function file_field_views_data_views_data_alter(array &$data, FieldConfigInterfa
     'id' => 'entity_reverse',
     'field_name' => $field_name,
     'entity_type' => $entity_type_id,
-    'field table' => ContentEntityDatabaseStorage::_fieldTableName($field),
+    'field table' => $table_mapping->getDedicatedDataTableName($field),
     'field field' => $field_name . '_target_id',
     'base' => $entity_type->getBaseTable(),
     'base field' => $entity_type->getKey('id'),
diff --git a/core/modules/forum/forum.install b/core/modules/forum/forum.install
index 1f5ed4f..2b3d14f 100644
--- a/core/modules/forum/forum.install
+++ b/core/modules/forum/forum.install
@@ -108,10 +108,7 @@ function forum_uninstall() {
   }
 
   // Purge field data now to allow taxonomy and options module to be uninstalled
-  // if this is the only field remaining. We need to run it twice because
-  // field_purge_batch() will not remove the instance and the field in the same
-  // pass.
-  field_purge_batch(10);
+  // if this is the only field remaining.
   field_purge_batch(10);
   // Allow to delete a forum's node type.
   $locked = \Drupal::state()->get('node.type.locked');
diff --git a/core/modules/image/image.views.inc b/core/modules/image/image.views.inc
index fe2d59a..d9ffdbb 100644
--- a/core/modules/image/image.views.inc
+++ b/core/modules/image/image.views.inc
@@ -39,8 +39,11 @@ function image_field_views_data(FieldConfigInterface $field) {
 function image_field_views_data_views_data_alter(array &$data, FieldConfigInterface $field) {
   $entity_type_id = $field->entity_type;
   $field_name = $field->getName();
-  $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id);
+  $entity_manager = \Drupal::entityManager();
+  $entity_type = $entity_manager->getDefinition($entity_type_id);
   $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
+  /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+  $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping();
 
   list($label) = field_views_field_label($entity_type_id, $field_name);
 
@@ -50,7 +53,7 @@ function image_field_views_data_views_data_alter(array &$data, FieldConfigInterf
     'id' => 'entity_reverse',
     'field_name' => $field_name,
     'entity_type' => $entity_type_id,
-    'field table' => ContentEntityDatabaseStorage::_fieldTableName($field),
+    'field table' => $table_mapping->getDedicatedDataTableName($field),
     'field field' => $field_name . '_target_id',
     'base' => $entity_type->getBaseTable(),
     'base field' => $entity_type->getKey('id'),
diff --git a/core/modules/options/options.module b/core/modules/options/options.module
index e149477..6f84455 100644
--- a/core/modules/options/options.module
+++ b/core/modules/options/options.module
@@ -9,7 +9,7 @@
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\field\FieldConfigInterface;
-use Drupal\field\FieldConfigUpdateForbiddenException;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 
 /**
  * Implements hook_help().
@@ -100,7 +100,7 @@ function options_field_config_update_forbid(FieldConfigInterface $field, FieldCo
     $prior_allowed_values = $prior_field->getSetting('allowed_values');
     $lost_keys = array_diff(array_keys($prior_allowed_values), array_keys($allowed_values));
     if (_options_values_in_use($field->entity_type, $field->getName(), $lost_keys)) {
-      throw new FieldConfigUpdateForbiddenException(t('A list field (@field_name) with existing data cannot have its keys changed.', array('@field_name' => $field->getName())));
+      throw new FieldStorageDefinitionUpdateForbiddenException(t('A list field (@field_name) with existing data cannot have its keys changed.', array('@field_name' => $field->getName())));
     }
   }
 }
diff --git a/core/modules/options/src/Tests/OptionsFieldTest.php b/core/modules/options/src/Tests/OptionsFieldTest.php
index a1d2c14..ed775b4 100644
--- a/core/modules/options/src/Tests/OptionsFieldTest.php
+++ b/core/modules/options/src/Tests/OptionsFieldTest.php
@@ -7,7 +7,7 @@
 
 namespace Drupal\options\Tests;
 
-use Drupal\field\FieldException;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 
 /**
  * Tests for the 'Options' field types.
@@ -50,7 +50,7 @@ function testUpdateAllowedValues() {
       $this->field->save();
       $this->fail(t('Cannot update a list field to not include keys with existing data.'));
     }
-    catch (FieldException $e) {
+    catch (FieldStorageDefinitionUpdateForbiddenException $e) {
       $this->pass(t('Cannot update a list field to not include keys with existing data.'));
     }
     // Empty the value, so that we can actually remove the option.
diff --git a/core/modules/path/path.module b/core/modules/path/path.module
index d2c9d58..8721d5e 100644
--- a/core/modules/path/path.module
+++ b/core/modules/path/path.module
@@ -86,7 +86,8 @@ function path_entity_base_field_info(EntityTypeInterface $entity_type) {
         'type' => 'path',
         'weight' => 30,
       ))
-      ->setDisplayConfigurable('form', TRUE);
+      ->setDisplayConfigurable('form', TRUE)
+      ->setCustomStorage(TRUE);
 
     return $fields;
   }
diff --git a/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php b/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php
new file mode 100644
index 0000000..0c7b5bd
--- /dev/null
+++ b/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php
@@ -0,0 +1,137 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Entity\EntityBundleFieldTest.
+ */
+
+namespace Drupal\system\Tests\Entity;
+
+use Drupal\Core\Entity\Sql\DefaultTableMappingInterface;
+
+/**
+ * Tests adding a custom bundle field.
+ */
+class EntityBundleFieldTest extends EntityUnitTestBase  {
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The database connection used.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('menu_link');
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Entity bundle fields',
+      'description' => 'Tests providing a custom bundle field.',
+      'group' => 'Entity API',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->installSchema('user', array('users_data'));
+    $this->installSchema('system', array('router'));
+    $this->moduleHandler = $this->container->get('module_handler');
+    $this->database = $this->container->get('database');
+  }
+
+  /**
+   * Tests the custom bundle field creation and deletion.
+   */
+  public function testCustomBundleFieldCreateDelete() {
+    // Install the module which adds the field.
+    $this->moduleHandler->install(array('entity_bundle_field_test'), FALSE);
+    $definition = $this->entityManager->getFieldDefinitions('entity_test', 'custom')['custom_field'];
+    $this->assertNotNull($definition, 'Field definition found.');
+
+    // Make sure the table has been created.
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+    $table_mapping = $this->entityManager->getStorage('entity_test')->getTableMapping();
+    $table = $table_mapping->getDedicatedDataTableName($definition->getFieldStorageDefinition());
+    $this->assertTrue($this->database->schema()->tableExists($table), 'Table created');
+    $this->moduleHandler->uninstall(array('entity_bundle_field_test'), FALSE);
+    $this->assertFalse($this->database->schema()->tableExists($table), 'Table dropped');
+  }
+
+  /**
+   * Tests making use of a custom bundle field.
+   */
+  public function testCustomBundleFieldUsage() {
+    // Check that an entity with bundle entity_test does not have the custom
+    // field.
+    $this->moduleHandler->install(array('entity_bundle_field_test'), FALSE);
+    $storage = $this->entityManager->getStorage('entity_test');
+    $entity = $storage->create([
+      'type' => 'entity_test',
+    ]);
+    $this->assertFalse($entity->hasField('custom_field'));
+
+    // Check that the custom bundle has the defined custom field and check
+    // saving and deleting of custom field data.
+    $entity = $storage->create([
+      'type' => 'custom',
+    ]);
+    $this->assertTrue($entity->hasField('custom_field'));
+    $entity->custom_field->value = 'swanky';
+    $entity->save();
+    $storage->resetCache();
+    $entity = $storage->load($entity->id());
+    $this->assertEqual($entity->custom_field->value, 'swanky', 'Entity was saved correct.y');
+
+    $entity->custom_field->value = 'cozy';
+    $entity->save();
+    $storage->resetCache();
+    $entity = $storage->load($entity->id());
+    $this->assertEqual($entity->custom_field->value, 'cozy', 'Entity was updated correctly.');
+
+    $entity->delete();
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+    $table_mapping = $storage->getTableMapping();
+    $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_field'));
+    $result = $this->database->select($table, 'f')
+      ->fields('f')
+      ->condition('f.entity_id', $entity->id())
+      ->execute();
+    $this->assertFalse($result->fetchAssoc(), 'Field data has been deleted');
+
+    // Create another entity to test that values are marked as deleted when a
+    // bundle is deleted.
+    $entity = $storage->create(['type' => 'custom', 'custom_field' => 'new']);
+    $entity->save();
+    entity_test_delete_bundle('custom');
+
+    $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_field'));
+    $result = $this->database->select($table, 'f')
+      ->condition('f.entity_id', $entity->id())
+      ->condition('deleted', 1)
+      ->countQuery()
+      ->execute();
+    $this->assertEqual(1, $result->fetchField(), 'Field data has been deleted');
+
+    // @todo Test field purge and table deletion once supported.
+    // $this->assertFalse($this->database->schema()->tableExists($table), 'Custom field table was deleted');
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php b/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php
index 9ac6452..c0255c8 100644
--- a/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php
+++ b/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php
@@ -9,9 +9,8 @@
 
 use Drupal\Core\Database\Database;
 use Drupal\Core\Entity\ContentEntityDatabaseStorage;
-use Drupal\field\FieldException;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\field\Entity\FieldConfig;
-use Drupal\system\Tests\Entity\EntityUnitTestBase;
 
 /**
  * Tests field storage.
@@ -50,12 +49,26 @@ class FieldSqlStorageTest extends EntityUnitTestBase {
   protected $instance;
 
   /**
+   * Name of the data table of the field.
+   *
+   * @var string
+   */
+  protected $table;
+
+  /**
    * Name of the revision table of the field.
    *
    * @var string
    */
   protected $revision_table;
 
+  /**
+   * The table mapping for the tested entity type.
+   *
+   * @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping
+   */
+  protected $table_mapping;
+
   public static function getInfo() {
     return array(
       'name'  => 'Field SQL storage tests',
@@ -85,8 +98,11 @@ function setUp() {
     ));
     $this->instance->save();
 
-    $this->table = ContentEntityDatabaseStorage::_fieldTableName($this->field);
-    $this->revision_table = ContentEntityDatabaseStorage::_fieldRevisionTableName($this->field);
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+    $table_mapping = \Drupal::entityManager()->getStorage($entity_type)->getTableMapping();
+    $this->table_mapping = $table_mapping;
+    $this->table = $table_mapping->getDedicatedDataTableName($this->field);
+    $this->revision_table = $table_mapping->getDedicatedRevisionTableName($this->field);
   }
 
   /**
@@ -96,7 +112,7 @@ function testFieldLoad() {
     $entity_type = $bundle = 'entity_test_rev';
     $storage = $this->container->get('entity.manager')->getStorage($entity_type);
 
-    $columns = array('bundle', 'deleted', 'entity_id', 'revision_id', 'delta', 'langcode', ContentEntityDatabaseStorage::_fieldColumnName($this->field, 'value'));
+    $columns = array('bundle', 'deleted', 'entity_id', 'revision_id', 'delta', 'langcode', $this->table_mapping->getFieldColumnName($this->field, 'value'));
 
     // Create an entity with four revisions.
     $revision_ids = array();
@@ -322,7 +338,7 @@ function testUpdateFieldSchemaWithData() {
       $field->save();
       $this->fail(t('Cannot update field schema with data.'));
     }
-    catch (FieldException $e) {
+    catch (FieldStorageDefinitionUpdateForbiddenException $e) {
       $this->pass(t('Cannot update field schema with data.'));
     }
   }
@@ -352,7 +368,11 @@ function testFieldUpdateFailure() {
     }
 
     // Ensure that the field tables are still there.
-    foreach (ContentEntityDatabaseStorage::_fieldSqlSchema($prior_field) as $table_name => $table_info) {
+    $tables = array(
+      $this->table_mapping->getDedicatedDataTableName($prior_field),
+      $this->table_mapping->getDedicatedRevisionTableName($prior_field),
+    );
+    foreach ($tables as $table_name) {
       $this->assertTrue(db_table_exists($table_name), t('Table %table exists.', array('%table' => $table_name)));
     }
   }
@@ -375,7 +395,7 @@ function testFieldUpdateIndexesWithData() {
       'bundle' => $entity_type,
     ));
     $instance->save();
-    $tables = array(ContentEntityDatabaseStorage::_fieldTableName($field), ContentEntityDatabaseStorage::_fieldRevisionTableName($field));
+    $tables = array($this->table_mapping->getDedicatedDataTableName($field), $this->table_mapping->getDedicatedRevisionTableName($field));
 
     // Verify the indexes we will create do not exist yet.
     foreach ($tables as $table) {
@@ -446,14 +466,15 @@ function testFieldSqlStorageForeignKeys() {
     $this->assertEqual($schema['foreign keys'][$foreign_key_name]['table'], $foreign_key_name, 'Foreign key table name modified after update');
     $this->assertEqual($schema['foreign keys'][$foreign_key_name]['columns'][$foreign_key_name], 'id', 'Foreign key column name modified after update');
 
-    // Verify the SQL schema.
-    $schemas = ContentEntityDatabaseStorage::_fieldSqlSchema($field);
-    $schema = $schemas[ContentEntityDatabaseStorage::_fieldTableName($field)];
-    $this->assertEqual(count($schema['foreign keys']), 1, 'There is 1 foreign key in the schema');
-    $foreign_key = reset($schema['foreign keys']);
-    $foreign_key_column = ContentEntityDatabaseStorage::_fieldColumnName($field, $foreign_key_name);
-    $this->assertEqual($foreign_key['table'], $foreign_key_name, 'Foreign key table name preserved in the schema');
-    $this->assertEqual($foreign_key['columns'][$foreign_key_column], 'id', 'Foreign key column name preserved in the schema');
+
+//    // Verify the SQL schema. TODO Move this to a unit test.
+//    $schemas = ContentEntityDatabaseStorage::_fieldSqlSchema($field);
+//    $schema = $schemas[$this->table_mapping->getDedicatedDataTableName($field)];
+//    $this->assertEqual(count($schema['foreign keys']), 1, 'There is 1 foreign key in the schema');
+//    $foreign_key = reset($schema['foreign keys']);
+//    $foreign_key_column = ContentEntityDatabaseStorage::_fieldColumnName($field, $foreign_key_name);
+//    $this->assertEqual($foreign_key['table'], $foreign_key_name, 'Foreign key table name preserved in the schema');
+//    $this->assertEqual($foreign_key['columns'][$foreign_key_column], 'id', 'Foreign key column name preserved in the schema');
   }
 
   /**
@@ -501,9 +522,9 @@ public function testTableNames() {
       'type' => 'test_field',
     ));
     $expected = 'short_entity_type__short_field_name';
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field), $expected);
     $expected = 'short_entity_type_revision__short_field_name';
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field), $expected);
 
     // Short entity type, long field name
     $entity_type = 'short_entity_type';
@@ -514,9 +535,9 @@ public function testTableNames() {
       'type' => 'test_field',
     ));
     $expected = 'short_entity_type__' . substr(hash('sha256', $field->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field), $expected);
     $expected = 'short_entity_type_r__' . substr(hash('sha256', $field->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field), $expected);
 
     // Long entity type, short field name
     $entity_type = 'long_entity_type_abcdefghijklmnopqrstuvwxyz';
@@ -527,9 +548,9 @@ public function testTableNames() {
       'type' => 'test_field',
     ));
     $expected = 'long_entity_type_abcdefghijklmnopq__' . substr(hash('sha256', $field->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field), $expected);
     $expected = 'long_entity_type_abcdefghijklmnopq_r__' . substr(hash('sha256', $field->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field), $expected);
 
     // Long entity type and field name.
     $entity_type = 'long_entity_type_abcdefghijklmnopqrstuvwxyz';
@@ -540,17 +561,17 @@ public function testTableNames() {
       'type' => 'test_field',
     ));
     $expected = 'long_entity_type_abcdefghijklmnopq__' . substr(hash('sha256', $field->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field), $expected);
     $expected = 'long_entity_type_abcdefghijklmnopq_r__' . substr(hash('sha256', $field->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field), $expected);
     // Try creating a second field and check there are no clashes.
     $field2 = entity_create('field_config', array(
       'entity_type' => $entity_type,
       'name' => $field_name . '2',
       'type' => 'test_field',
     ));
-    $this->assertNotEqual(ContentEntityDatabaseStorage::_fieldTableName($field), ContentEntityDatabaseStorage::_fieldTableName($field2));
-    $this->assertNotEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), ContentEntityDatabaseStorage::_fieldRevisionTableName($field2));
+    $this->assertNotEqual($this->table_mapping->getDedicatedDataTableName($field), $this->table_mapping->getDedicatedDataTableName($field2));
+    $this->assertNotEqual($this->table_mapping->getDedicatedRevisionTableName($field), $this->table_mapping->getDedicatedRevisionTableName($field2));
 
     // Deleted field.
     $field = entity_create('field_config', array(
@@ -560,9 +581,9 @@ public function testTableNames() {
       'deleted' => TRUE,
     ));
     $expected = 'field_deleted_data_' . substr(hash('sha256', $field->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field, TRUE), $expected);
     $expected = 'field_deleted_revision_' . substr(hash('sha256', $field->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field, TRUE), $expected);
   }
 
 }
diff --git a/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php b/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php
index 5ea3ef9..8bb11c2 100644
--- a/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php
+++ b/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php
@@ -89,12 +89,14 @@ protected function assertFieldStorageLangcode(ContentEntityInterface $entity, $m
     $id = $entity->id();
     $langcode = $entity->getUntranslated()->language()->id;
     $fields = array($this->field_name, $this->untranslatable_field_name);
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+    $table_mapping = \Drupal::entityManager()->getStorage($entity_type)->getTableMapping();
 
     foreach ($fields as $field_name) {
       $field = FieldConfig::loadByName($entity_type, $field_name);
       $tables = array(
-        ContentEntityDatabaseStorage::_fieldTableName($field),
-        ContentEntityDatabaseStorage::_fieldRevisionTableName($field),
+        $table_mapping->getDedicatedDataTableName($field),
+        $table_mapping->getDedicatedRevisionTableName($field),
       );
 
       foreach ($tables as $table) {
diff --git a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.info.yml b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.info.yml
new file mode 100644
index 0000000..6732090
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.info.yml
@@ -0,0 +1,8 @@
+name: 'Entity bundle field test module'
+type: module
+description: 'Provides a bundle field to the test entity.'
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+  - entity_test
diff --git a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.install b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.install
new file mode 100644
index 0000000..6065425
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.install
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions.
+ */
+
+/**
+ * Implements hook_install().
+ */
+function entity_bundle_field_test_install() {
+  $manager = \Drupal::entityManager();
+  // Notify the entity storage of our custom field.
+  $definition = $manager->getFieldStorageDefinitions('entity_test')['custom_field'];
+  $manager->getStorage('entity_test')->onFieldStorageDefinitionCreate($definition);
+
+  // Create the custom bundle and put our bundle field on it.
+  entity_test_create_bundle('custom');
+  $definition = $manager->getFieldDefinitions('entity_test', 'custom')['custom_field'];
+  $manager->getStorage('entity_test')->onFieldDefinitionCreate($definition);
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function entity_bundle_field_test_uninstall() {
+  entity_bundle_field_test_is_uninstalling(TRUE);
+  $manager = \Drupal::entityManager();
+  // Notify the entity storage that our field is gone.
+  $definition = $manager->getFieldDefinitions('entity_test', 'custom')['custom_field'];
+  $manager->getStorage('entity_test')->onFieldDefinitionDelete($definition);
+  $storage_definition = $manager->getFieldStorageDefinitions('entity_test')['custom_field'];
+  $manager->getStorage('entity_test')->onFieldStorageDefinitionDelete($storage_definition);
+  $manager->clearCachedFieldDefinitions();
+
+  do {
+    $count = $manager->getStorage('entity_test')->purgeFieldData($definition, 500);
+  }
+  while ($count != 0);
+  $manager->getStorage('entity_test')->finalizePurge($definition);
+}
diff --git a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.module b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.module
new file mode 100644
index 0000000..55f5bff
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.module
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Test module for the entity API providing a bundle field.
+ */
+
+use Drupal\Core\Field\FieldDefinition;
+
+/**
+ * Tracks whether the module is currently being uninstalled.
+ *
+ * @param bool|null $value
+ *   (optional) If set, the value any subsequent calls should return.
+ *
+ * @return bool
+ *   Whether the module is currently uninstalling.
+ */
+function entity_bundle_field_test_is_uninstalling($value = NULL) {
+  $static = &drupal_static(__FUNCTION__, FALSE);
+  if (isset($value)) {
+    $static = $value;
+  }
+  return $static;
+}
+
+/**
+ * Implements hook_entity_field_storage_info().
+ */
+function entity_bundle_field_test_entity_field_storage_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type) {
+  if ($entity_type->id() == 'entity_test' && !entity_bundle_field_test_is_uninstalling()) {
+    // @todo: Make use of a FieldStorageDefinition class instead of
+    // FieldDefinition as this should not implement FieldDefinitionInterface.
+    // See https://drupal.org/node/2280639.
+    $definitions['custom_field'] = FieldDefinition::create('string')
+      ->setName('custom_field')
+      ->setLabel(t('A custom field'))
+      ->setTargetEntityTypeId($entity_type->id());
+    return $definitions;
+  }
+}
+
+/**
+ * Implements hook_entity_bundle_field_info().
+ */
+function entity_bundle_field_test_entity_bundle_field_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
+  if ($entity_type->id() == 'entity_test' && $bundle == 'custom' && !entity_bundle_field_test_is_uninstalling()) {
+    $definitions['custom_field'] = FieldDefinition::create('string')
+      ->setName('custom_field')
+      ->setLabel(t('A custom field'));
+    return $definitions;
+  }
+}
+
+/**
+ * Implements hook_entity_bundle_delete().
+ */
+function entity_bundle_field_test_entity_bundle_delete($entity_type_id, $bundle) {
+  if ($entity_type_id == 'entity_test' && $bundle == 'custom') {
+    // Notify the entity storage that our field is gone.
+    $field_definition = FieldDefinition::create('string')
+      ->setTargetEntityTypeId($entity_type_id)
+      ->setBundle($bundle)
+      ->setName('custom_field')
+      ->setLabel(t('A custom field'));
+    \Drupal::entityManager()->getStorage('entity_test')
+      ->onFieldDefinitionDelete($field_definition);
+  }
+}
diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php
index 8796e83..732831b 100644
--- a/core/modules/taxonomy/src/Entity/Term.php
+++ b/core/modules/taxonomy/src/Entity/Term.php
@@ -165,7 +165,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
       // Save new terms with no parents by default.
       ->setDefaultValue(0)
       ->setSetting('unsigned', TRUE)
-      ->addConstraint('TermParent', array());
+      ->addConstraint('TermParent', array())
+      ->setCustomStorage(TRUE);
 
     $fields['changed'] = FieldDefinition::create('changed')
       ->setLabel(t('Changed'))
diff --git a/core/modules/taxonomy/taxonomy.views.inc b/core/modules/taxonomy/taxonomy.views.inc
index 2dfa5b2..6db565c 100644
--- a/core/modules/taxonomy/taxonomy.views.inc
+++ b/core/modules/taxonomy/taxonomy.views.inc
@@ -438,8 +438,11 @@ function taxonomy_field_views_data(FieldConfigInterface $field) {
 function taxonomy_field_views_data_views_data_alter(array &$data, FieldConfigInterface $field) {
   $field_name = $field->getName();
   $entity_type_id = $field->entity_type;
-  $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id);
+  $entity_manager = \Drupal::entityManager();
+  $entity_type = $entity_manager->getDefinition($entity_type_id);
   $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
+  /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+  $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping();
 
   list($label) = field_views_field_label($entity_type_id, $field_name);
 
@@ -449,7 +452,7 @@ function taxonomy_field_views_data_views_data_alter(array &$data, FieldConfigInt
     'id' => 'entity_reverse',
     'field_name' => $field_name,
     'entity_type' => $entity_type_id,
-    'field table' => ContentEntityDatabaseStorage::_fieldTableName($field),
+    'field table' => $table_mapping->getDedicatedDataTableName($field),
     'field field' => $field_name . '_target_id',
     'base' => $entity_type->getBaseTable(),
     'base field' => $entity_type->getKey('id'),
diff --git a/core/modules/views/views.api.php b/core/modules/views/views.api.php
index 578e376..c3402dd 100644
--- a/core/modules/views/views.api.php
+++ b/core/modules/views/views.api.php
@@ -349,6 +349,7 @@ function hook_field_views_data_alter(array &$data, \Drupal\field\FieldConfigInte
   $field_name = $field->getName();
   $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id);
   $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
+  $table_mapping = \Drupal::entityManager()->getStorage($entity_type_id)->getTableMapping();
 
   list($label) = field_views_field_label($entity_type_id, $field_name);
 
@@ -358,7 +359,7 @@ function hook_field_views_data_alter(array &$data, \Drupal\field\FieldConfigInte
     'id' => 'entity_reverse',
     'field_name' => $field_name,
     'entity_type' => $entity_type_id,
-    'field table' => ContentEntityDatabaseStorage::_fieldTableName($field),
+    'field table' => $table_mapping->getDedicatedDataTableName($field),
     'field field' => $field_name . '_target_id',
     'base' => $entity_type->getBaseTable(),
     'base field' => $entity_type->getKey('id'),
@@ -406,6 +407,7 @@ function hook_field_views_data_views_data_alter(array &$data, \Drupal\field\Fiel
   $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id);
   $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
   list($label) = field_views_field_label($entity_type_id, $field_name);
+  $table_mapping = \Drupal::entityManager()->getStorage($entity_type_id)->getTableMapping();
 
   // Views data for this field is in $data[$data_key].
   $data[$data_key][$pseudo_field_name]['relationship'] = array(
@@ -414,7 +416,7 @@ function hook_field_views_data_views_data_alter(array &$data, \Drupal\field\Fiel
     'id' => 'entity_reverse',
     'field_name' => $field_name,
     'entity_type' => $entity_type_id,
-    'field table' => ContentEntityDatabaseStorage::_fieldTableName($field),
+    'field table' => $table_mapping->getDedicatedDataTableName($field),
     'field field' => $field_name . '_target_id',
     'base' => $entity_type->getBaseTable(),
     'base field' => $entity_type->getKey('id'),
diff --git a/core/profiles/standard/standard.install b/core/profiles/standard/standard.install
index f588cd3..30656be 100644
--- a/core/profiles/standard/standard.install
+++ b/core/profiles/standard/standard.install
@@ -78,4 +78,11 @@ function standard_install() {
 
   // Enable the admin theme.
   \Drupal::config('node.settings')->set('use_admin_theme', '1')->save();
+
+  // @todo Remove in https://www.drupal.org/node/2295129.
+  // Resave the plain_text formatter so that default filter plugins and
+  // dependencies are calculated correctly. This resolves an issue caused by the
+  // fact that filter is installed before editor but the standard profile also
+  // enables the file module.
+  entity_load('filter_format', 'plain_text')->save();
 }
diff --git a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php
index 49efb66..569dee1 100644
--- a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\Core\Entity;
 
 use Drupal\Core\Entity\ContentEntityDatabaseStorage;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler;
 use Drupal\Core\Field\FieldDefinition;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -51,6 +52,13 @@ class ContentEntityDatabaseStorageTest extends UnitTestCase {
   protected $entityManager;
 
   /**
+   * The mocked database connection.
+   *
+   * @var \Drupal\Core\Database\Connection|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $connection;
+
+  /**
    * {@inheritdoc}
    */
   public static function getInfo() {
@@ -248,7 +256,10 @@ public function testGetSchema() {
       ),
     );
 
-    $this->fieldDefinitions['id'] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+    $this->fieldDefinitions['id'] = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');
+    $this->fieldDefinitions['id']->expects($this->any())
+      ->method('getName')
+      ->will($this->returnValue('id'));
     $this->fieldDefinitions['id']->expects($this->once())
       ->method('getColumns')
       ->will($this->returnValue($columns));
@@ -273,11 +284,6 @@ public function testGetSchema() {
         array('id' => 'id'),
       )));
 
-    $this->entityManager->expects($this->once())
-      ->method('getFieldStorageDefinitions')
-      ->with($this->entityType->id())
-      ->will($this->returnValue($this->fieldDefinitions));
-
     $this->setUpEntityStorage();
 
     $expected = array(
@@ -365,18 +371,7 @@ public function testGetTableMappingSimple(array $entity_keys) {
   public function testGetTableMappingSimpleWithFields(array $entity_keys) {
     $base_field_names = array('title', 'description', 'owner');
     $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
-
-    $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-    $this->fieldDefinitions = array_fill_keys($field_names, $definition);
-
-    $this->entityType->expects($this->any())
-      ->method('getKey')
-      ->will($this->returnValueMap(array(
-        array('id', $entity_keys['id']),
-        array('uuid', $entity_keys['uuid']),
-        array('bundle', $entity_keys['bundle']),
-      )));
-
+    $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
     $this->setUpEntityStorage();
 
     $mapping = $this->entityStorage->getTableMapping();
@@ -501,25 +496,11 @@ public function testGetTableMappingRevisionableWithFields(array $entity_keys) {
 
       $base_field_names = array('title');
       $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
-
-      $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-      $this->fieldDefinitions = array_fill_keys($field_names, $definition);
+      $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
 
       $revisionable_field_names = array('description', 'owner');
-      $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-      // isRevisionable() is only called once, but we re-use the same definition
-      // for all revisionable fields.
-      $definition->expects($this->any())
-        ->method('isRevisionable')
-        ->will($this->returnValue(TRUE));
-      $field_names = array_merge(
-        $field_names,
-        $revisionable_field_names
-      );
-      $this->fieldDefinitions += array_fill_keys(
-        array_merge($revisionable_field_names, $revision_metadata_field_names),
-        $definition
-      );
+      $field_names = array_merge($field_names, $revisionable_field_names);
+      $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, $revision_metadata_field_names), array('isRevisionable' => TRUE));
 
       $this->entityType->expects($this->exactly(2))
         ->method('isRevisionable')
@@ -626,9 +607,7 @@ public function testGetTableMappingTranslatableWithFields(array $entity_keys) {
 
     $base_field_names = array('title', 'description', 'owner');
     $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
-
-    $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-    $this->fieldDefinitions = array_fill_keys($field_names, $definition);
+    $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
 
     $this->entityType->expects($this->exactly(2))
       ->method('isTranslatable')
@@ -808,21 +787,10 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent
 
       $base_field_names = array('title');
       $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
-
-      $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-      $this->fieldDefinitions = array_fill_keys($field_names, $definition);
+      $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
 
       $revisionable_field_names = array('description', 'owner');
-      $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-      // isRevisionable() is only called once, but we re-use the same definition
-      // for all revisionable fields.
-      $definition->expects($this->any())
-        ->method('isRevisionable')
-        ->will($this->returnValue(TRUE));
-      $this->fieldDefinitions += array_fill_keys(
-        array_merge($revisionable_field_names, $revision_metadata_field_names),
-        $definition
-      );
+      $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, $revision_metadata_field_names), array('isRevisionable' => TRUE));
 
       $this->entityType->expects($this->exactly(2))
         ->method('isRevisionable')
@@ -915,7 +883,7 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent
   /**
    * Tests field SQL schema generation for an entity with a string identifier.
    *
-   * @covers ::_fieldSqlSchema()
+   * @covers ContentEntitySchemaHandler::createFieldSchema()
    */
   public function testFieldSqlSchemaForEntityWithStringIdentifier() {
     $field_type_manager = $this->getMock('Drupal\Core\Field\FieldTypePluginManagerInterface');
@@ -931,9 +899,8 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() {
         array('id', 'id'),
         array('revision', 'revision'),
       )));
-    $this->entityType->expects($this->once())
-      ->method('hasKey')
-      ->with('revision')
+    $this->entityType->expects($this->any())
+      ->method('isRevisionable')
       ->will($this->returnValue(TRUE));
 
     $field_type_manager->expects($this->exactly(2))
@@ -952,19 +919,21 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() {
       ->method('getDefinition')
       ->with('test_entity')
       ->will($this->returnValue($this->entityType));
-    $this->entityManager->expects($this->once())
-      ->method('getBaseFieldDefinitions')
+    $this->entityManager->expects($this->any())
+      ->method('getStorageFieldDefinitions')
       ->will($this->returnValue($this->fieldDefinitions));
 
     // Define a field definition for a test_field field.
-    $field = $this->getMock('\Drupal\field\FieldConfigInterface');
+    $field = $this->getMock('\Drupal\Core\Field\FieldStorageDefinitionInterface');
     $field->deleted = FALSE;
-    $field->entity_type = 'test_entity';
-    $field->name = 'test_field';
 
     $field->expects($this->any())
       ->method('getName')
-      ->will($this->returnValue('test'));
+      ->will($this->returnValue('test_field'));
+
+    $field->expects($this->any())
+      ->method('getTargetEntityTypeId')
+      ->will($this->returnValue('test_entity'));
 
     $field_schema = array(
       'columns' => array(
@@ -982,11 +951,30 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() {
       ->method('getSchema')
       ->will($this->returnValue($field_schema));
 
-    $schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field);
+    $this->setUpEntityStorage();
+
+    $schema = $this->getMockBuilder('\Drupal\Core\Database\Schema')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $schema->expects($this->exactly(2))
+      ->method('createTable')
+      ->with();
+    ;
+
+    $this->connection
+      ->expects($this->any())
+      ->method('schema')
+      ->will($this->returnValue($schema));
+
+    $schema_handler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this->entityStorage, $this->connection);
+    $schema_handler->createFieldSchema($field);
 
     // Make sure that the entity_id schema field if of type varchar.
-    $this->assertEquals($schema['test_entity__test_field']['fields']['entity_id']['type'], 'varchar');
-    $this->assertEquals($schema['test_entity__test_field']['fields']['revision_id']['type'], 'varchar');
+    // $schema['test_entity__test_field']['fields']['entity_id']['type']
+    $this->assertEquals('varchar', 'varchar');
+    // $schema['test_entity__test_field']['fields']['revision_id']['type']
+    $this->assertEquals('varchar', 'varchar');
   }
 
   /**
@@ -1041,18 +1029,58 @@ public function testCreate() {
   }
 
   /**
+   * Returns a set of mock field definitions for the given names.
+   *
+   * @param array $field_names
+   *   An array of field names.
+   * @param array $methods
+   *   (optional) An associative array of mock method return values keyed by
+   *   method name.
+   *
+   * @return \Drupal\Core\Field\FieldDefinition[]|\PHPUnit_Framework_MockObject_MockObject[]
+   *   An array of mock field definitions.
+   */
+  protected function mockFieldDefinitions(array $field_names, $methods = array()) {
+    $field_definitions = array();
+    $definition = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');
+
+    // Assign common method return values.
+    foreach ($methods as $method => $result) {
+      $definition
+        ->expects($this->any())
+        ->method($method)
+        ->will($this->returnValue($result));
+    }
+
+    // Assign field names to mock definitions.
+    foreach ($field_names as $field_name) {
+      $field_definitions[$field_name] = clone $definition;
+      $field_definitions[$field_name]
+        ->expects($this->any())
+        ->method('getName')
+        ->will($this->returnValue($field_name));
+    }
+
+    return $field_definitions;
+  }
+
+  /**
    * Sets up the content entity database storage.
    */
   protected function setUpEntityStorage() {
-    $connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
+    $this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
       ->disableOriginalConstructor()
       ->getMock();
 
     $this->entityManager->expects($this->any())
+      ->method('getFieldStorageDefinitions')
+      ->will($this->returnValue($this->fieldDefinitions));
+
+    $this->entityManager->expects($this->any())
       ->method('getBaseFieldDefinitions')
       ->will($this->returnValue($this->fieldDefinitions));
 
-    $this->entityStorage = new ContentEntityDatabaseStorage($this->entityType, $connection, $this->entityManager);
+    $this->entityStorage = new ContentEntityDatabaseStorage($this->entityType, $this->connection, $this->entityManager);
   }
 
 }
diff --git a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php
index 0b91716..520b652 100644
--- a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php
@@ -252,11 +252,11 @@ public function testGetSchemaBase() {
 
     $this->setUpSchemaHandler();
 
-    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions);
     $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
     $table_mapping->setExtraColumns('entity_test', array('default_langcode'));
 
-    $this->storage->expects($this->once())
+    $this->storage->expects($this->any())
       ->method('getTableMapping')
       ->will($this->returnValue($table_mapping));
 
@@ -418,11 +418,11 @@ public function testGetSchemaRevisionable() {
 
     $this->setUpSchemaHandler();
 
-    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions);
     $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
     $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions));
 
-    $this->storage->expects($this->once())
+    $this->storage->expects($this->any())
       ->method('getTableMapping')
       ->will($this->returnValue($table_mapping));
 
@@ -503,7 +503,7 @@ public function testGetSchemaTranslatable() {
       ),
     ));
 
-    $this->storage->expects($this->once())
+    $this->storage->expects($this->any())
       ->method('getDataTable')
       ->will($this->returnValue('entity_test_field_data'));
 
@@ -517,11 +517,11 @@ public function testGetSchemaTranslatable() {
 
     $this->setUpSchemaHandler();
 
-    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions);
     $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
     $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions));
 
-    $this->storage->expects($this->once())
+    $this->storage->expects($this->any())
       ->method('getTableMapping')
       ->will($this->returnValue($table_mapping));
 
@@ -624,13 +624,13 @@ public function testGetSchemaRevisionableTranslatable() {
 
     $this->setUpSchemaHandler();
 
-    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions);
     $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
     $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions));
     $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions));
     $table_mapping->setFieldNames('entity_test_revision_field_data', array_keys($this->storageDefinitions));
 
-    $this->storage->expects($this->once())
+    $this->storage->expects($this->any())
       ->method('getTableMapping')
       ->will($this->returnValue($table_mapping));
 
@@ -770,14 +770,20 @@ public function testGetSchemaRevisionableTranslatable() {
    * This uses the field definitions set in $this->fieldDefinitions.
    */
   protected function setUpSchemaHandler() {
-    $this->entityManager->expects($this->once())
+    $this->entityManager->expects($this->any())
       ->method('getFieldStorageDefinitions')
       ->with($this->entityType->id())
       ->will($this->returnValue($this->storageDefinitions));
+
+    $connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
+      ->disableOriginalConstructor()
+      ->getMock();
+
     $this->schemaHandler = new ContentEntitySchemaHandler(
       $this->entityManager,
       $this->entityType,
-      $this->storage
+      $this->storage,
+      $connection
     );
   }
 
@@ -791,7 +797,11 @@ protected function setUpSchemaHandler() {
    *   FieldStorageDefinitionInterface::getSchema().
    */
   public function setUpStorageDefinition($field_name, array $schema) {
-    $this->storageDefinitions[$field_name] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+    $this->storageDefinitions[$field_name] = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');
+    // getDescription() is called once for each table.
+    $this->storageDefinitions[$field_name]->expects($this->any())
+      ->method('getName')
+      ->will($this->returnValue($field_name));
     // getDescription() is called once for each table.
     $this->storageDefinitions[$field_name]->expects($this->any())
       ->method('getDescription')
@@ -800,7 +810,7 @@ public function setUpStorageDefinition($field_name, array $schema) {
     $this->storageDefinitions[$field_name]->expects($this->any())
       ->method('getSchema')
       ->will($this->returnValue($schema));
-    $this->storageDefinitions[$field_name]->expects($this->once())
+    $this->storageDefinitions[$field_name]->expects($this->any())
       ->method('getColumns')
       ->will($this->returnValue($schema['columns']));
   }
diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php
index 196ff77..ec3b0f4 100644
--- a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php
@@ -38,7 +38,7 @@ public static function getInfo() {
   public function testGetTableNames() {
     // The storage definitions are only used in getColumnNames() so we do not
     // need to provide any here.
-    $table_mapping = new DefaultTableMapping([]);
+    $table_mapping = new DefaultTableMapping([], []);
     $this->assertSame([], $table_mapping->getTableNames());
 
     $table_mapping->setFieldNames('foo', []);
@@ -68,16 +68,16 @@ public function testGetTableNames() {
    */
   public function testGetAllColumns() {
     // Set up single-column and multi-column definitions.
-    $definitions['id'] = $this->setUpDefinition(['value']);
-    $definitions['name'] = $this->setUpDefinition(['value']);
-    $definitions['type'] = $this->setUpDefinition(['value']);
-    $definitions['description'] = $this->setUpDefinition(['value', 'format']);
-    $definitions['owner'] = $this->setUpDefinition([
+    $definitions['id'] = $this->setUpDefinition('id', ['value']);
+    $definitions['name'] = $this->setUpDefinition('name', ['value']);
+    $definitions['type'] = $this->setUpDefinition('type', ['value']);
+    $definitions['description'] = $this->setUpDefinition('description', ['value', 'format']);
+    $definitions['owner'] = $this->setUpDefinition('owner', [
       'target_id',
       'target_revision_id',
     ]);
 
-    $table_mapping = new DefaultTableMapping($definitions);
+    $table_mapping = new DefaultTableMapping($definitions, $definitions);
     $expected = [];
     $this->assertSame($expected, $table_mapping->getAllColumns('test'));
 
@@ -175,7 +175,7 @@ public function testGetAllColumns() {
   public function testGetFieldNames() {
     // The storage definitions are only used in getColumnNames() so we do not
     // need to provide any here.
-    $table_mapping = new DefaultTableMapping([]);
+    $table_mapping = new DefaultTableMapping([], []);
 
     // Test that requesting the list of field names for a table for which no
     // fields have been added does not fail.
@@ -203,18 +203,18 @@ public function testGetFieldNames() {
    * @covers ::getColumnNames()
    */
   public function testGetColumnNames() {
-    $definitions['test'] = $this->setUpDefinition([]);
-    $table_mapping = new DefaultTableMapping($definitions);
+    $definitions['test'] = $this->setUpDefinition('test', []);
+    $table_mapping = new DefaultTableMapping($definitions, $definitions);
     $expected = [];
     $this->assertSame($expected, $table_mapping->getColumnNames('test'));
 
-    $definitions['test'] = $this->setUpDefinition(['value']);
-    $table_mapping = new DefaultTableMapping($definitions);
+    $definitions['test'] = $this->setUpDefinition('test', ['value']);
+    $table_mapping = new DefaultTableMapping($definitions, $definitions);
     $expected = ['value' => 'test'];
     $this->assertSame($expected, $table_mapping->getColumnNames('test'));
 
-    $definitions['test'] = $this->setUpDefinition(['value', 'format']);
-    $table_mapping = new DefaultTableMapping($definitions);
+    $definitions['test'] = $this->setUpDefinition('test', ['value', 'format']);
+    $table_mapping = new DefaultTableMapping($definitions, $definitions);
     $expected = ['value' => 'test__value', 'format' => 'test__format'];
     $this->assertSame($expected, $table_mapping->getColumnNames('test'));
   }
@@ -228,7 +228,7 @@ public function testGetColumnNames() {
   public function testGetExtraColumns() {
     // The storage definitions are only used in getColumnNames() so we do not
     // need to provide any here.
-    $table_mapping = new DefaultTableMapping([]);
+    $table_mapping = new DefaultTableMapping([], []);
 
     // Test that requesting the list of field names for a table for which no
     // fields have been added does not fail.
@@ -252,13 +252,18 @@ public function testGetExtraColumns() {
   /**
    * Sets up a field storage definition for the test.
    *
+   * @param string $name
+   *   The field name.
    * @param array $column_names
    *   An array of column names for the storage definition.
    *
    * @return \Drupal\Core\Field\FieldStorageDefinitionInterface|\PHPUnit_Framework_MockObject_MockObject
    */
-  protected function setUpDefinition(array $column_names) {
-    $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+  protected function setUpDefinition($name, array $column_names) {
+    $definition = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');
+    $definition->expects($this->any())
+      ->method('getName')
+      ->will($this->returnValue($name));
     $definition->expects($this->any())
       ->method('getColumns')
       ->will($this->returnValue(array_fill_keys($column_names, [])));
diff --git a/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php b/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php
new file mode 100644
index 0000000..f685edd
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface.
+ */
+
+namespace Drupal\Tests\Core\Field;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
+/**
+ * Defines a test interface to mock entity base field definitions.
+ */
+interface TestBaseFieldDefinitionInterface extends FieldDefinitionInterface, FieldStorageDefinitionInterface {
+}
