diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
index 09fcf36b910c928768cbd329d7eb6576ec8ae9f7..9c2c8936edc263f1402c0aa625843bd46224f5d1 100644
--- a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
@@ -189,8 +189,13 @@ protected function getChangeList() {
         // Detect updated field storage definitions.
         foreach (array_intersect_key($storage_definitions, $original_storage_definitions) as $field_name => $storage_definition) {
           // @todo Support non-storage-schema-changing definition updates too:
-          //   https://www.drupal.org/node/2336895.
-          if ($this->requiresFieldStorageSchemaChanges($storage_definition, $original_storage_definitions[$field_name])) {
+          //   https://www.drupal.org/node/2336895. So long as we're checking
+          //   based on schema change requirements rather than definition
+          //   equality, skip the check if the entity type itself needs to be
+          //   updated, since that can affect the schema of all fields, so we
+          //   want to process that update first without reporting false
+          //   positives here.
+          if (!isset($change_list[$entity_type_id]['entity_type']) && $this->requiresFieldStorageSchemaChanges($storage_definition, $original_storage_definitions[$field_name])) {
             $field_changes[$field_name] = static::DEFINITION_UPDATED;
           }
         }
diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php
index a2aedf397bf1c07076d1517dd0bf8e757778aeed..66ef2a6a1f28a73df07c30656096025dd32f9052 100644
--- a/core/lib/Drupal/Core/Entity/EntityManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityManager.php
@@ -1165,7 +1165,7 @@ protected function deleteLastInstalledDefinition($entity_type_id) {
    * {@inheritdoc}
    */
   public function getLastInstalledFieldStorageDefinitions($entity_type_id) {
-    return $this->installedDefinitions->get($entity_type_id . '.field_storage_definitions');
+    return $this->installedDefinitions->get($entity_type_id . '.field_storage_definitions', array());
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Entity/Schema/FieldableEntityStorageSchemaInterface.php b/core/lib/Drupal/Core/Entity/Schema/FieldableEntityStorageSchemaInterface.php
index 759fb6e8c1fca0b78dcc99b5288e0fec001f7500..c20d9d1d9268201cac51717340ff9f058aa89660 100644
--- a/core/lib/Drupal/Core/Entity/Schema/FieldableEntityStorageSchemaInterface.php
+++ b/core/lib/Drupal/Core/Entity/Schema/FieldableEntityStorageSchemaInterface.php
@@ -67,4 +67,12 @@ public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterfac
    */
   public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original);
 
+  /**
+   * Performs final cleanup after all data of a field has been purged.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field being purged.
+   */
+  public function finalizePurge(FieldStorageDefinitionInterface $storage_definition);
+
 }
diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
index 4e1546bb005be586b927b86f38528e8e296b1712..f992081ad5814cf80812512d31ba48b593c6230f 100644
--- a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
+++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
@@ -15,7 +15,7 @@
 class DefaultTableMapping implements TableMappingInterface {
 
   /**
-   * A list of field storage definitions that are available for this mapping.
+   * The field storage definitions of this mapping.
    *
    * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
    */
@@ -101,7 +101,16 @@ 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])) {
+        // Unlike in shared storage tables, in dedicated ones field columns are
+        // positioned last.
+        $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];
   }
@@ -179,6 +188,47 @@ public function setExtraColumns($table_name, array $column_names) {
     return $this;
   }
 
+  /**
+   * 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.
+   */
+  public function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
+    return !$storage_definition->hasCustomStorage() && $storage_definition->isBaseField() && !$storage_definition->isMultiple();
+  }
+
+  /**
+   * Checks whether the given field has to be stored in a dedicated 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.
+   */
+  public function requiresDedicatedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
+    return !$storage_definition->hasCustomStorage() && !$this->allowsSharedTableStorage($storage_definition);
+  }
+
+  /**
+   * Returns a list of dedicated table names for this mapping.
+   *
+   * @return string[]
+   *   An array of table names.
+   */
+  public function getDedicatedTableNames() {
+    $table_mapping = $this;
+    $definitions = array_filter($this->fieldStorageDefinitions, function($definition) use ($table_mapping) { return $table_mapping->requiresDedicatedTableStorage($definition); });
+    $data_tables = array_map(function($definition) use ($table_mapping) { return $table_mapping->getDedicatedDataTableName($definition); }, $definitions);
+    $revision_tables = array_map(function($definition) use ($table_mapping) { return $table_mapping->getDedicatedRevisionTableName($definition); }, $definitions);
+    $dedicated_tables = array_merge(array_values($data_tables), array_values($revision_tables));
+    return $dedicated_tables;
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index e88025c94ada565a302dd29e98051dd771531718..1ccd508a22462024d4b7c32b1fdca9071912a8a1 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -17,12 +17,12 @@
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\Core\Entity\Query\QueryInterface;
 use Drupal\Core\Entity\Schema\FieldableEntityStorageSchemaInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Language\LanguageInterface;
+use Drupal\field\FieldStorageConfigInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -33,9 +33,8 @@
  *
  * The class uses \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
  * internally in order to automatically generate the database schema based on
- * the defined base fields. Entity types can override
- * SqlContentEntityStorage::getSchema() to customize the generated
- * schema; e.g., to add additional indexes.
+ * the defined base fields. Entity types can override the schema handler to
+ * customize the generated schema; e.g., to add additional indexes.
  *
  * @ingroup entity_api
  */
@@ -107,11 +106,11 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
   protected $entityManager;
 
   /**
-   * The entity schema handler.
+   * The entity type's storage schema object.
    *
    * @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
    */
-  protected $schemaHandler;
+  protected $storageSchema;
 
   /**
    * Cache backend.
@@ -157,15 +156,27 @@ public function getFieldStorageDefinitions() {
    */
   public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache) {
     parent::__construct($entity_type);
-
     $this->database = $database;
     $this->entityManager = $entity_manager;
     $this->cacheBackend = $cache;
+    $this->initTableLayout();
+  }
+
+  /**
+   * Initializes table name variables.
+   */
+  protected function initTableLayout() {
+    // Reset table field values to ensure changes in the entity type definition
+    // are correctly reflected in the table layout.
+    $this->tableMapping = NULL;
+    $this->revisionKey = NULL;
+    $this->revisionTable = NULL;
+    $this->dataTable = NULL;
+    $this->revisionDataTable = NULL;
 
     // @todo Remove table names from the entity type definition in
     //   https://drupal.org/node/2232465
     $this->baseTable = $this->entityType->getBaseTable() ?: $this->entityTypeId;
-
     $revisionable = $this->entityType->isRevisionable();
     if ($revisionable) {
       $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
@@ -223,32 +234,57 @@ public function getRevisionDataTable() {
   }
 
   /**
-   * Gets the schema handler for this entity storage.
+   * Returns the entity type's storage schema object.
    *
    * @return \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
-   *   The schema handler.
+   *   The schema object.
    */
-  protected function schemaHandler() {
-    if (!isset($this->schemaHandler)) {
-      $schema_handler_class = $this->entityType->getHandlerClass('storage_schema') ?: 'Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema';
-      $this->schemaHandler = new $schema_handler_class($this->entityManager, $this->entityType, $this, $this->database);
+  protected function getStorageSchema() {
+    if (!isset($this->storageSchema)) {
+      $class = $this->entityType->getHandlerClass('storage_schema') ?: 'Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema';
+      $this->storageSchema = new $class($this->entityManager, $this->entityType, $this, $this->database);
     }
-    return $this->schemaHandler;
+    return $this->storageSchema;
   }
 
   /**
-   * {@inheritdoc}
+   * Updates the wrapped entity type definition.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The update entity type.
+   *
+   * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
+   *   See https://www.drupal.org/node/2274017.
    */
-  public function getTableMapping() {
-    if (!isset($this->tableMapping)) {
+  public function setEntityType(EntityTypeInterface $entity_type) {
+    if ($this->entityType->id() == $entity_type->id()) {
+      $this->entityType = $entity_type;
+      $this->initTableLayout();
+    }
+    else {
+      throw new EntityStorageException(String::format('Unsupported entity type @id', array('@id' => $entity_type->id())));
+    }
+  }
 
-      $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();
+  /**
+   * {@inheritdoc}
+   */
+  public function getTableMapping(array $storage_definitions = NULL) {
+    $table_mapping = $this->tableMapping;
+
+    // If we are using our internal storage definitions, which is our main use
+    // case, we can statically cache the computed table mapping. If a new set
+    // of field storage definitions is passed, for instance when comparing old
+    // and new storage schema, we compute the table mapping without caching.
+    // @todo Clean-up this in https://www.drupal.org/node/2274017 so we can
+    //   easily instantiate a new table mapping whenever needed.
+    if (!isset($this->tableMapping) || $storage_definitions) {
+      $definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
+      $table_mapping = new DefaultTableMapping($definitions);
+
+      $definitions = array_filter($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);
@@ -272,16 +308,16 @@ public function getTableMapping() {
       $translatable = $this->entityType->isTranslatable();
       if (!$revisionable && !$translatable) {
         // The base layout stores all the base field values in the base table.
-        $this->tableMapping->setFieldNames($this->baseTable, $all_fields);
+        $table_mapping->setFieldNames($this->baseTable, $all_fields);
       }
       elseif ($revisionable && !$translatable) {
         // The revisionable layout stores all the base field values in the base
         // table, except for revision metadata fields. Revisionable fields
         // denormalized in the base table but also stored in the revision table
         // together with the entity ID and the revision ID as identifiers.
-        $this->tableMapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields));
+        $table_mapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields));
         $revision_key_fields = array($this->idKey, $this->revisionKey);
-        $this->tableMapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
+        $table_mapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
       }
       elseif (!$revisionable && $translatable) {
         // Multilingual layouts store key field values in the base table. The
@@ -290,7 +326,7 @@ public function getTableMapping() {
         // denormalized copy of the bundle field value to allow for more
         // performant queries. This means that only the UUID is not stored on
         // the data table.
-        $this->tableMapping
+        $table_mapping
           ->setFieldNames($this->baseTable, $key_fields)
           ->setFieldNames($this->dataTable, array_values(array_diff($all_fields, array($this->uuidKey))))
           // Add the denormalized 'default_langcode' field to the mapping. Its
@@ -306,13 +342,13 @@ public function getTableMapping() {
         // holds the data field values for all non-revisionable fields. The data
         // field values of revisionable fields are denormalized in the data
         // table, as well.
-        $this->tableMapping->setFieldNames($this->baseTable, array_values(array_diff($key_fields, array($this->langcodeKey))));
+        $table_mapping->setFieldNames($this->baseTable, array_values(array_diff($key_fields, array($this->langcodeKey))));
 
         // Like in the multilingual, non-revisionable case the UUID is not
         // in the data table. Additionally, do not store revision metadata
         // fields in the data table.
         $data_fields = array_values(array_diff($all_fields, array($this->uuidKey), $revision_metadata_fields));
-        $this->tableMapping
+        $table_mapping
           ->setFieldNames($this->dataTable, $data_fields)
           // Add the denormalized 'default_langcode' field to the mapping. Its
           // value is identical to the query expression
@@ -321,20 +357,45 @@ public function getTableMapping() {
           ->setExtraColumns($this->dataTable, array('default_langcode'));
 
         $revision_base_fields = array_merge(array($this->idKey, $this->revisionKey, $this->langcodeKey), $revision_metadata_fields);
-        $this->tableMapping->setFieldNames($this->revisionTable, $revision_base_fields);
+        $table_mapping->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);
-        $this->tableMapping
+        $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, array($this->langcodeKey));
+        $table_mapping
           ->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields))
           // Add the denormalized 'default_langcode' field to the mapping. Its
           // value is identical to the query expression
           // "revision_table.langcode = data_table.langcode".
           ->setExtraColumns($this->revisionDataTable, array('default_langcode'));
       }
+
+      // Add dedicated tables.
+      $definitions = array_filter($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);
+        }
+      }
+
+      // Cache the computed table mapping only if we are using our internal
+      // storage definitions.
+      if (!$storage_definitions) {
+        $this->tableMapping = $table_mapping;
+      }
     }
 
-    return $this->tableMapping;
+    return $table_mapping;
   }
 
   /**
@@ -581,7 +642,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);
@@ -1150,7 +1211,7 @@ protected function loadFieldItems(array $entities) {
       $definitions[$bundle] = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle);
       foreach ($definitions[$bundle] as $field_name => $field_definition) {
         $storage_definition = $field_definition->getFieldStorageDefinition();
-        if ($this->usesDedicatedTable($storage_definition)) {
+        if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
           $storage_definitions[$field_name] = $storage_definition;
         }
       }
@@ -1225,7 +1286,7 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
 
     foreach ($this->entityManager->getFieldDefinitions($entity_type, $bundle) as $field_name => $field_definition) {
       $storage_definition = $field_definition->getFieldStorageDefinition();
-      if (!$this->usesDedicatedTable($storage_definition)) {
+      if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
         continue;
       }
       $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
@@ -1240,10 +1301,12 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
             ->condition('entity_id', $id)
             ->execute();
         }
-        $this->database->delete($revision_name)
-          ->condition('entity_id', $id)
-          ->condition('revision_id', $vid)
-          ->execute();
+        if ($this->entityType->isRevisionable()) {
+          $this->database->delete($revision_name)
+            ->condition('entity_id', $id)
+            ->condition('revision_id', $vid)
+            ->execute();
+        }
       }
 
       // Prepare the multi-insert query.
@@ -1253,7 +1316,9 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
         $columns[] = $table_mapping->getFieldColumnName($storage_definition, $column);
       }
       $query = $this->database->insert($table_name)->fields($columns);
-      $revision_query = $this->database->insert($revision_name)->fields($columns);
+      if ($this->entityType->isRevisionable()) {
+        $revision_query = $this->database->insert($revision_name)->fields($columns);
+      }
 
       $langcodes = $field_definition->isTranslatable() ? $translation_langcodes : array($default_langcode);
       foreach ($langcodes as $langcode) {
@@ -1276,7 +1341,9 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
             $record[$column_name] = !empty($attributes['serialize']) ? serialize($item->$column) : $item->$column;
           }
           $query->values($record);
-          $revision_query->values($record);
+          if ($this->entityType->isRevisionable()) {
+            $revision_query->values($record);
+          }
 
           if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) {
             break;
@@ -1291,7 +1358,9 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
         if ($entity->isDefaultRevision()) {
           $query->execute();
         }
-        $revision_query->execute();
+        if ($this->entityType->isRevisionable()) {
+          $revision_query->execute();
+        }
       }
     }
   }
@@ -1306,7 +1375,7 @@ protected function deleteFieldItems(EntityInterface $entity) {
     $table_mapping = $this->getTableMapping();
     foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
       $storage_definition = $field_definition->getFieldStorageDefinition();
-      if (!$this->usesDedicatedTable($storage_definition)) {
+      if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
         continue;
       }
       $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
@@ -1314,9 +1383,11 @@ protected function deleteFieldItems(EntityInterface $entity) {
       $this->database->delete($table_name)
         ->condition('entity_id', $entity->id())
         ->execute();
-      $this->database->delete($revision_name)
-        ->condition('entity_id', $entity->id())
-        ->execute();
+      if ($this->entityType->isRevisionable()) {
+        $this->database->delete($revision_name)
+          ->condition('entity_id', $entity->id())
+          ->execute();
+      }
     }
   }
 
@@ -1332,7 +1403,7 @@ protected function deleteFieldItemsRevision(EntityInterface $entity) {
       $table_mapping = $this->getTableMapping();
       foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
         $storage_definition = $field_definition->getFieldStorageDefinition();
-        if (!$this->usesDedicatedTable($storage_definition)) {
+        if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
           continue;
         }
         $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
@@ -1344,185 +1415,109 @@ protected function deleteFieldItemsRevision(EntityInterface $entity) {
     }
   }
 
-  /**
-   * Returns whether the field uses a dedicated table for storage.
-   *
-   * @param FieldStorageDefinitionInterface $definition
-   *   The field storage definition.
-   *
-   * @return bool
-   *   Whether the field uses a dedicated table for storage.
-   */
-  protected function usesDedicatedTable(FieldStorageDefinitionInterface $definition) {
-    // Everything that is not provided by the entity type is stored in a
-    // dedicated table.
-    return $definition->getProvider() != $this->entityType->getProvider() && !$definition->hasCustomStorage();
-  }
-
   /**
    * {@inheritdoc}
    */
   public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
-    return $this->schemaHandler()->requiresEntityStorageSchemaChanges($entity_type, $original);
+    return $this->getStorageSchema()->requiresEntityStorageSchemaChanges($entity_type, $original);
   }
 
   /**
    * {@inheritdoc}
    */
   public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
-    return $this->schemaHandler()->requiresFieldStorageSchemaChanges($storage_definition, $original);
+    return $this->getStorageSchema()->requiresFieldStorageSchemaChanges($storage_definition, $original);
   }
 
   /**
    * {@inheritdoc}
    */
   public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
-    return $this->schemaHandler()->requiresEntityDataMigration($entity_type, $original);
+    return $this->getStorageSchema()->requiresEntityDataMigration($entity_type, $original);
   }
 
   /**
    * {@inheritdoc}
    */
   public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
-    return $this->schemaHandler()->requiresFieldDataMigration($storage_definition, $original);
+    return $this->getStorageSchema()->requiresFieldDataMigration($storage_definition, $original);
   }
 
   /**
    * {@inheritdoc}
    */
   public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
-    $this->schemaHandler()->onEntityTypeCreate($entity_type);
+    $this->getStorageSchema()->onEntityTypeCreate($entity_type);
   }
 
   /**
    * {@inheritdoc}
    */
   public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
-    $this->schemaHandler()->onEntityTypeUpdate($entity_type, $original);
+    // Ensure we have an updated entity type definition.
+    $this->entityType = $entity_type;
+    // The table layout may have changed depending on the new entity type
+    // definition.
+    $this->initTableLayout();
+    // Let the schema handler adapt to possible table layout changes.
+    $this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
   }
 
   /**
    * {@inheritdoc}
    */
   public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
-    $this->schemaHandler()->onEntityTypeDelete($entity_type);
+    $this->getStorageSchema()->onEntityTypeDelete($entity_type);
   }
 
   /**
    * {@inheritdoc}
    */
   public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
-    $schema = $this->_fieldSqlSchema($storage_definition);
-    foreach ($schema as $name => $table) {
-      $this->database->schema()->createTable($name, $table);
-    }
+    // If we are adding a field stored in a shared table we need to recompute
+    // the table mapping.
+    // @todo This does not belong here. Remove it once we are able to generate a
+    //   fresh table mapping in the schema handler. See
+    //   https://www.drupal.org/node/2274017.
+    if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
+      $this->tableMapping = NULL;
+    }
+    $this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
   }
 
   /**
    * {@inheritdoc}
    */
   public function onFieldStorageDefinitionUpdate(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->_fieldSqlSchema($original);
-        foreach ($original_schema as $name => $table) {
-          $this->database->schema()->dropTable($name, $table);
-        }
-        $schema = $this->_fieldSqlSchema($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->_fieldSqlSchema($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->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 = static::_fieldIndexName($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 = static::_fieldIndexName($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);
-        }
-      }
-    }
+    $this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
   }
 
   /**
    * {@inheritdoc}
    */
   public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
-    $table_mapping = $this->getTableMapping();
+    $table_mapping = $this->getTableMapping(
+      $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
+    );
 
-    // 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();
+    // @todo Remove the FieldStorageConfigInterface check when non-configurable
+    //   fields support purging: https://www.drupal.org/node/2282119.
+    if ($storage_definition instanceof FieldStorageConfigInterface && $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();
+      if ($this->entityType->isRevisionable()) {
+        $this->database->update($revision_table)
+          ->fields(array('deleted' => 1))
+          ->execute();
+      }
+    }
 
-    // Move the table to a unique name while the table contents are being
-    // deleted.
-    $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);
+    // Update the field schema.
+    $this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
   }
 
   /**
@@ -1532,16 +1527,20 @@ public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definiti
     $table_mapping = $this->getTableMapping();
     $storage_definition = $field_definition->getFieldStorageDefinition();
     // Mark field data as deleted.
-    $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
-    $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
-    $this->database->update($table_name)
-      ->fields(array('deleted' => 1))
-      ->condition('bundle', $field_definition->getBundle())
-      ->execute();
-    $this->database->update($revision_name)
-      ->fields(array('deleted' => 1))
-      ->condition('bundle', $field_definition->getBundle())
-      ->execute();
+    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('deleted' => 1))
+        ->condition('bundle', $field_definition->getBundle())
+        ->execute();
+      if ($this->entityType->isRevisionable()) {
+        $this->database->update($revision_name)
+          ->fields(array('deleted' => 1))
+          ->condition('bundle', $field_definition->getBundle())
+          ->execute();
+      }
+    }
   }
 
   /**
@@ -1560,7 +1559,7 @@ public function onBundleRename($bundle, $bundle_new) {
 
     foreach ($field_definitions as $field_definition) {
       $storage_definition = $field_definition->getFieldStorageDefinition();
-      if ($this->usesDedicatedTable($storage_definition)) {
+      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);
@@ -1568,10 +1567,12 @@ public function onBundleRename($bundle, $bundle_new) {
           ->fields(array('bundle' => $bundle_new))
           ->condition('bundle', $bundle)
           ->execute();
-        $this->database->update($revision_name)
-          ->fields(array('bundle' => $bundle_new))
-          ->condition('bundle', $bundle)
-          ->execute();
+        if ($this->entityType->isRevisionable()) {
+          $this->database->update($revision_name)
+            ->fields(array('bundle' => $bundle_new))
+            ->condition('bundle', $bundle)
+            ->execute();
+        }
       }
     }
   }
@@ -1649,20 +1650,18 @@ protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefiniti
     $this->database->delete($table_name)
       ->condition('revision_id', $revision_id)
       ->execute();
-    $this->database->delete($revision_name)
-      ->condition('revision_id', $revision_id)
-      ->execute();
+    if ($this->entityType->isRevisionable()) {
+      $this->database->delete($revision_name)
+        ->condition('revision_id', $revision_id)
+        ->execute();
+    }
   }
 
   /**
    * {@inheritdoc}
    */
   public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
-    $table_mapping = $this->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);
+    $this->getStorageSchema()->finalizePurge($storage_definition);
   }
 
   /**
@@ -1671,23 +1670,49 @@ public function finalizePurge(FieldStorageDefinitionInterface $storage_definitio
   public function countFieldData($storage_definition, $as_bool = FALSE) {
     $table_mapping = $this->getTableMapping();
 
-    $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
-    $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);
+    if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+      $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
+      $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);
+    }
+    elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
+      $data_table = $this->dataTable ?: $this->baseTable;
+      $query = $this->database->select($data_table, 't');
+      $columns = $storage_definition->getColumns();
+      if (count($columns) > 1) {
+        $or = $query->orConditionGroup();
+        foreach ($columns as $column_name => $data) {
+          $or->isNotNull($storage_definition->getName() . '__' . $column_name);
+        }
+        $query->condition($or);
+      }
+      else {
+        $query->isNotNull($storage_definition->getName());
+      }
+      $query
+        ->fields('t', array($this->idKey))
+        ->distinct(TRUE);
+    }
+
+    // @todo Find a way to count field data also for fields having custom
+    //   storage. See https://www.drupal.org/node/2337753.
+    $count = 0;
+    if (isset($query)) {
+      // 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();
     }
-    $count = $query->countQuery()->execute()->fetchField();
     return $as_bool ? (bool) $count : (int) $count;
   }
 
@@ -1701,200 +1726,7 @@ public function countFieldData($storage_definition, $as_bool = FALSE) {
    *   Whether the field has been already deleted.
    */
   protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) {
-    return !array_key_exists($storage_definition->getName(), $this->entityManager->getFieldStorageDefinitions($this->entityTypeId));
-  }
-
-  /**
-   * 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\Core\Field\FieldStorageDefinitionInterface $storage_definition
-   *   The field storage definition.
-   * @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()
-   */
-  public static function _fieldSqlSchema(FieldStorageDefinitionInterface $storage_definition, array $schema = NULL, $deleted = FALSE) {
-    $table_mapping = new DefaultTableMapping(array());
-
-    $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()}.";
-
-    $entity_type_id = $storage_definition->getTargetEntityTypeId();
-    $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'),
-      ),
-    );
-
-    if (!$schema) {
-      $schema = $storage_definition->getSchema();
-    }
-
-    // Add field columns.
-    foreach ($schema['columns'] as $column_name => $attributes) {
-      $real_name = $table_mapping->getFieldColumnName($storage_definition, $column_name);
-      $current['fields'][$real_name] = $attributes;
-    }
-
-    // Add unique keys.
-    foreach ($schema['unique keys'] as $unique_key_name => $columns) {
-      $real_name = static::_fieldIndexName($storage_definition, $unique_key_name);
-      foreach ($columns as $column_name) {
-        $current['unique keys'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
-      }
-    }
-
-    // Add indexes.
-    foreach ($schema['indexes'] as $index_name => $columns) {
-      $real_name = static::_fieldIndexName($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)) {
-          $current['indexes'][$real_name][] = array(
-            $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
-            $column_name[1],
-          );
-        }
-        else {
-          $current['indexes'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
-        }
-      }
-    }
-
-    // Add foreign keys.
-    foreach ($schema['foreign keys'] as $specifier => $specification) {
-      $real_name = static::_fieldIndexName($storage_definition, $specifier);
-      $current['foreign keys'][$real_name]['table'] = $specification['table'];
-      foreach ($specification['columns'] as $column_name => $referenced) {
-        $sql_storage_column = $table_mapping->getFieldColumnName($storage_definition, $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(
-      $table_mapping->getDedicatedDataTableName($storage_definition) => $current,
-      $table_mapping->getDedicatedRevisionTableName($storage_definition) => $revision,
-    );
-  }
-
-  /**
-   * 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.
-   */
-  public static function _fieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
-    return $storage_definition->getName() . '_' . $index;
+    return !array_key_exists($storage_definition->getName(), $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityTypeId));
   }
 
 }
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
index ca61630448448b1dc4dc7f6d497c1a66caee5086..de6910f011abf22049e590b3bff0fb6aa273e561 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -13,14 +13,29 @@
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\Core\Entity\Schema\FieldableEntityStorageSchemaInterface;
+use Drupal\Core\Field\FieldException;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\field\FieldStorageConfigInterface;
 
 /**
  * Defines a schema handler that supports revisionable, translatable entities.
+ *
+ * Entity types may extend this class and optimize the generated schema for all
+ * entity base tables by overriding getEntitySchema() for cross-field
+ * optimizations and getSharedTableFieldSchema() for optimizations applying to
+ * a single field.
  */
 class SqlContentEntityStorageSchema implements FieldableEntityStorageSchemaInterface {
 
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
   /**
    * The entity type this schema builder is responsible for.
    *
@@ -35,6 +50,14 @@ class SqlContentEntityStorageSchema implements FieldableEntityStorageSchemaInter
    */
   protected $fieldStorageDefinitions;
 
+  /**
+   * The original storage field definitions for this entity type. Used during
+   * field schema updates.
+   *
+   * @var \Drupal\Core\Field\FieldDefinitionInterface[]
+   */
+  protected $originalDefinitions;
+
   /**
    * The storage object for the given entity type.
    *
@@ -56,6 +79,13 @@ class SqlContentEntityStorageSchema implements FieldableEntityStorageSchemaInter
    */
   protected $database;
 
+  /**
+   * The key-value collection for tracking installed storage schema.
+   *
+   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+   */
+  protected $installedStorageSchema;
+
   /**
    * Constructs a SqlContentEntityStorageSchema.
    *
@@ -69,32 +99,79 @@ class SqlContentEntityStorageSchema implements FieldableEntityStorageSchemaInter
    *   The database connection to be used.
    */
   public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, SqlContentEntityStorage $storage, Connection $database) {
+    $this->entityManager = $entity_manager;
     $this->entityType = $entity_type;
     $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id());
     $this->storage = $storage;
     $this->database = $database;
   }
 
+  /**
+   * Returns the keyvalue collection for tracking the installed schema.
+   *
+   * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+   *
+   * @todo Inject this dependency in the constructor once this class can be
+   *   instantiated as a regular entity handler:
+   *   https://www.drupal.org/node/2332857.
+   */
+  protected function installedStorageSchema() {
+    if (!isset($this->installedStorageSchema)) {
+      $this->installedStorageSchema = \Drupal::keyValue('entity.storage_schema.sql');
+    }
+    return $this->installedStorageSchema;
+  }
+
   /**
    * {@inheritdoc}
    */
   public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
     return
       $entity_type->getStorageClass() != $original->getStorageClass() ||
-      $entity_type->getKeys() != $original->getKeys() ||
       $entity_type->isRevisionable() != $original->isRevisionable() ||
-      $entity_type->isTranslatable() != $original->isTranslatable();
+      $entity_type->isTranslatable() != $original->isTranslatable() ||
+      // Detect changes in key or index definitions.
+      $this->getEntitySchemaData($entity_type, $this->getEntitySchema($entity_type, TRUE)) != $this->loadEntitySchemaData($original);
   }
 
   /**
    * {@inheritdoc}
    */
   public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
-    return
+    $table_mapping = $this->storage->getTableMapping();
+
+    if (
       $storage_definition->hasCustomStorage() != $original->hasCustomStorage() ||
       $storage_definition->getSchema() != $original->getSchema() ||
       $storage_definition->isRevisionable() != $original->isRevisionable() ||
-      $storage_definition->isTranslatable() != $original->isTranslatable();
+      $storage_definition->isTranslatable() != $original->isTranslatable() ||
+      $table_mapping->allowsSharedTableStorage($storage_definition) != $table_mapping->allowsSharedTableStorage($original) ||
+      $table_mapping->requiresDedicatedTableStorage($storage_definition) != $table_mapping->requiresDedicatedTableStorage($original)
+    ) {
+      return TRUE;
+    }
+
+    if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+      return $this->getDedicatedTableSchema($storage_definition) != $this->loadFieldSchemaData($original);
+    }
+    elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
+      $field_name = $storage_definition->getName();
+      $schema = array();
+      foreach (array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()) as $table_name) {
+        if (in_array($field_name, $table_mapping->getFieldNames($table_name))) {
+          $column_names = $table_mapping->getColumnNames($storage_definition->getName());
+          $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
+        }
+      }
+      return $schema != $this->loadFieldSchemaData($original);
+    }
+    else {
+      // The field has custom storage, so we don't know if a schema change is
+      // needed or not, but since per the initial checks earlier in this
+      // function, nothing about the definition changed that we manage, we
+      // return FALSE.
+      return FALSE;
+    }
   }
 
   /**
@@ -117,23 +194,14 @@ public function requiresEntityDataMigration(EntityTypeInterface $entity_type, En
       // @todo Ask the old storage handler rather than assuming:
       //   https://www.drupal.org/node/2335879.
       $entity_type->getStorageClass() != $original_storage_class ||
-      !$this->tableIsEmpty($this->storage->getBaseTable());
+      !$this->isTableEmpty($this->storage->getBaseTable());
   }
 
   /**
    * {@inheritdoc}
    */
   public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
-    // If the base table is empty, there are no entities, and therefore, no
-    // field data that we care about preserving.
-    // @todo We might be returning TRUE here in cases where it would be safe
-    //   to return FALSE (for example, if the field is in a dedicated table
-    //   and that table is empty), and thereby preventing automatic updates
-    //   that should be possible, but determining that requires refactoring
-    //   SqlContentEntityStorage::_fieldSqlSchema(), and in the meantime,
-    //   it's safer to return false positives than false negatives:
-    //   https://www.drupal.org/node/1498720.
-    return !$this->tableIsEmpty($this->storage->getBaseTable());
+    return !$this->storage->countFieldData($original, TRUE);
   }
 
   /**
@@ -142,12 +210,31 @@ public function requiresFieldDataMigration(FieldStorageDefinitionInterface $stor
   public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
     $this->checkEntityType($entity_type);
     $schema_handler = $this->database->schema();
+
+    // Create entity tables.
     $schema = $this->getEntitySchema($entity_type, TRUE);
     foreach ($schema as $table_name => $table_schema) {
       if (!$schema_handler->tableExists($table_name)) {
         $schema_handler->createTable($table_name, $table_schema);
       }
     }
+
+    // Create dedicated field tables.
+    $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type->id());
+    $table_mapping = $this->storage->getTableMapping($field_storage_definitions);
+    foreach ($field_storage_definitions as $field_storage_definition) {
+      if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
+        $this->createDedicatedTableSchema($field_storage_definition);
+      }
+      elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
+        // The shared tables are already fully created, but we need to save the
+        // per-field schema definitions for later use.
+        $this->createSharedTableSchema($field_storage_definition, TRUE);
+      }
+    }
+
+    // Save data about entity indexes and keys.
+    $this->saveEntitySchemaData($entity_type, $schema);
   }
 
   /**
@@ -162,8 +249,13 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
       return;
     }
 
+    // If a migration is required, we can't proceed.
+    if ($this->requiresEntityDataMigration($entity_type, $original)) {
+      throw new EntityStorageException(String::format('The SQL storage cannot change the schema for an existing entity type with data.'));
+    }
+
     // If we have no data just recreate the entity schema from scratch.
-    if (!$this->requiresEntityDataMigration($entity_type, $original)) {
+    if ($this->isTableEmpty($this->storage->getBaseTable())) {
       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.
@@ -184,9 +276,40 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
         throw $e;
       }
     }
-    // Otherwise, throw an exception.
     else {
-      throw new EntityStorageException(String::format('The SQL storage cannot change the schema for an existing entity type with data.'));
+      $schema_handler = $this->database->schema();
+
+      // Drop original indexes and unique keys.
+      foreach ($this->loadEntitySchemaData($entity_type) as $table_name => $schema) {
+        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);
+          }
+        }
+      }
+
+      // Create new indexes and unique keys.
+      $entity_schema = $this->getEntitySchema($entity_type, TRUE);
+      foreach ($this->getEntitySchemaData($entity_type, $entity_schema) as $table_name => $schema) {
+        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);
+          }
+        }
+      }
+
+      // Store the updated entity schema.
+      $this->saveEntitySchemaData($entity_type, $entity_schema);
     }
   }
 
@@ -196,38 +319,95 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
   public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
     $this->checkEntityType($entity_type);
     $schema_handler = $this->database->schema();
+    $actual_definition = $this->entityManager->getDefinition($entity_type->id());
+    // @todo Instead of switching the wrapped entity type, we should be able to
+    //   instantiate a new table mapping for each entity type definition. See
+    //   https://www.drupal.org/node/2274017.
+    $this->storage->setEntityType($entity_type);
+
+    // Delete entity tables.
     foreach ($this->getEntitySchemaTables() as $table_name) {
       if ($schema_handler->tableExists($table_name)) {
         $schema_handler->dropTable($table_name);
       }
     }
+
+    // Delete dedicated field tables.
+    $field_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type->id());
+    $this->originalDefinitions = $field_storage_definitions;
+    $table_mapping = $this->storage->getTableMapping($field_storage_definitions);
+    foreach ($field_storage_definitions as $field_storage_definition) {
+      if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
+        $this->deleteDedicatedTableSchema($field_storage_definition);
+      }
+    }
+    $this->originalDefinitions = NULL;
+
+    $this->storage->setEntityType($actual_definition);
+
+    // Delete the entity schema.
+    $this->deleteEntitySchemaData($entity_type);
   }
 
   /**
    * {@inheritdoc}
    */
   public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
-    // @todo Move implementation from
-    //   SqlContentEntityStorage::onFieldStorageDefinitionCreate()
-    //   into here: https://www.drupal.org/node/1498720
+    $this->performFieldSchemaOperation('create', $storage_definition);
   }
 
   /**
    * {@inheritdoc}
    */
   public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
-    // @todo Move implementation from
-    //   SqlContentEntityStorage::onFieldStorageDefinitionUpdate()
-    //   into here: https://www.drupal.org/node/1498720
+    // Store original definitions so that switching between shared and dedicated
+    // field table layout works.
+    $this->originalDefinitions = $this->fieldStorageDefinitions;
+    $this->originalDefinitions[$original->getName()] = $original;
+    $this->performFieldSchemaOperation('update', $storage_definition, $original);
+    $this->originalDefinitions = NULL;
   }
 
   /**
    * {@inheritdoc}
    */
   public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
-    // @todo Move implementation from
-    //   SqlContentEntityStorage::onFieldStorageDefinitionDelete()
-    //   into here: https://www.drupal.org/node/1498720
+    // Only configurable fields currently support purging, so prevent deletion
+    // of ones we can't purge if they have existing data.
+    // @todo Add purging to all fields: https://www.drupal.org/node/2282119.
+    if (!($storage_definition instanceof FieldStorageConfigInterface) && $this->storage->countFieldData($storage_definition, TRUE)) {
+      throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field with data that can\'t be purged.');
+    }
+
+    // Retrieve a table mapping which contains the deleted field still.
+    $table_mapping = $this->storage->getTableMapping(
+      $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
+    );
+    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);
+      $new_table = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
+      $this->database->schema()->renameTable($table, $new_table);
+      if ($this->entityType->isRevisionable()) {
+        $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
+        $revision_new_table = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE);
+        $this->database->schema()->renameTable($revision_table, $revision_new_table);
+      }
+    }
+
+    // @todo Remove when finalizePurge() is invoked from the outside for all
+    //   fields: https://www.drupal.org/node/2282119.
+    if (!($storage_definition instanceof FieldStorageConfigInterface)) {
+      $this->performFieldSchemaOperation('delete', $storage_definition);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
+    $this->performFieldSchemaOperation('delete', $storage_definition);
   }
 
   /**
@@ -251,6 +431,12 @@ protected function checkEntityType(EntityTypeInterface $entity_type) {
   /**
    * Returns the entity schema for the specified entity type.
    *
+   * Entity types may override this method in order to optimize the generated
+   * schema of the entity tables. However, only cross-field optimizations should
+   * be added here; e.g., an index spanning multiple fields. Optimizations that
+   * apply to a single field have to be added via
+   * SqlContentEntityStorageSchema::getSharedTableFieldSchema() instead.
+   *
    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
    *   The entity type definition.
    * @param bool $reset
@@ -260,30 +446,54 @@ protected function checkEntityType(EntityTypeInterface $entity_type) {
    * @return array
    *   A Schema API array describing the entity schema, excluding dedicated
    *   field tables.
+   *
+   * @throws \Drupal\Core\Field\FieldException
    */
   protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
+    $this->checkEntityType($entity_type);
     $entity_type_id = $entity_type->id();
 
     if (!isset($this->schema[$entity_type_id]) || $reset) {
-      // Initialize the table schema.
+      // Back up the storage definition and replace it with the passed one.
+      // @todo Instead of switching the wrapped entity type, we should be able
+      //   to instantiate a new table mapping for each entity type definition.
+      //   See https://www.drupal.org/node/2274017.
+      $actual_definition = $this->entityManager->getDefinition($entity_type_id);
+      $this->storage->setEntityType($entity_type);
+
+      // Prepare basic information about the entity type.
       $tables = $this->getEntitySchemaTables();
-      $schema[$tables['base_table']] = $this->initializeBaseTable();
+
+      // Initialize the table schema.
+      $schema[$tables['base_table']] = $this->initializeBaseTable($entity_type);
       if (isset($tables['revision_table'])) {
-        $schema[$tables['revision_table']] = $this->initializeRevisionTable();
+        $schema[$tables['revision_table']] = $this->initializeRevisionTable($entity_type);
       }
       if (isset($tables['data_table'])) {
-        $schema[$tables['data_table']] = $this->initializeDataTable();
+        $schema[$tables['data_table']] = $this->initializeDataTable($entity_type);
       }
       if (isset($tables['revision_data_table'])) {
-        $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable();
+        $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable($entity_type);
       }
 
+      // We need to act only on shared entity schema tables.
       $table_mapping = $this->storage->getTableMapping();
-      foreach ($table_mapping->getTableNames() as $table_name) {
-        // Add the schema from field definitions.
+      $table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
+      $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
+      foreach ($table_names as $table_name) {
+        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);
+          if (!isset($storage_definitions[$field_name])) {
+            throw new FieldException(String::format('Field storage definition for "@field_name" could not be found.', array('@field_name' => $field_name)));
+          }
+          // Add the schema for base field definitions.
+          elseif ($table_mapping->allowsSharedTableStorage($storage_definitions[$field_name])) {
+            $column_names = $table_mapping->getColumnNames($field_name);
+            $storage_definition = $storage_definitions[$field_name];
+            $schema[$table_name] = array_merge_recursive($schema[$table_name], $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names));
+          }
         }
 
         // Add the schema for extra fields.
@@ -295,18 +505,21 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
       }
 
       // Process tables after having gathered field information.
-      $this->processBaseTable($schema[$tables['base_table']]);
+      $this->processBaseTable($entity_type, $schema[$tables['base_table']]);
       if (isset($tables['revision_table'])) {
-        $this->processRevisionTable($schema[$tables['revision_table']]);
+        $this->processRevisionTable($entity_type, $schema[$tables['revision_table']]);
       }
       if (isset($tables['data_table'])) {
-        $this->processDataTable($schema[$tables['data_table']]);
+        $this->processDataTable($entity_type, $schema[$tables['data_table']]);
       }
       if (isset($tables['revision_data_table'])) {
-        $this->processRevisionDataTable($schema[$tables['revision_data_table']]);
+        $this->processRevisionDataTable($entity_type, $schema[$tables['revision_data_table']]);
       }
 
       $this->schema[$entity_type_id] = $schema;
+
+      // Restore the actual definition.
+      $this->storage->setEntityType($actual_definition);
     }
 
     return $this->schema[$entity_type_id];
@@ -328,55 +541,41 @@ protected function getEntitySchemaTables() {
   }
 
   /**
-   * Returns the schema for a single field definition.
+   * Returns entity schema definitions for index and key definitions.
    *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type 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 string[] $column_mapping
-   *   A mapping of field column names to database column names.
+   *   The entity schema array.
+   *
+   * @return array
+   *   A stripped down version of the $schema Schema API array containing, for
+   *   each table, only the key and index definitions not derived from field
+   *   storage definitions.
    */
-  protected function addFieldSchema(array &$schema, $field_name, array $column_mapping) {
-    $field_schema = $this->fieldStorageDefinitions[$field_name]->getSchema();
-    $field_description = $this->fieldStorageDefinitions[$field_name]->getDescription();
-
-    foreach ($column_mapping as $field_column_name => $schema_field_name) {
-      $column_schema = $field_schema['columns'][$field_column_name];
+  protected function getEntitySchemaData(ContentEntityTypeInterface $entity_type, array $schema) {
+    $schema_data = array();
+    $entity_type_id = $entity_type->id();
+    $keys = array('indexes', 'unique keys');
+    $unused_keys = array_flip(array('description', 'fields', 'foreign keys'));
 
-      $schema['fields'][$schema_field_name] = $column_schema;
-      $schema['fields'][$schema_field_name]['description'] = $field_description;
-      // Only entity keys are required.
-      $keys = $this->entityType->getKeys() + array('langcode' => 'langcode');
-      // The label is an entity key, but label fields are not necessarily
-      // required.
-      // Because entity ID and revision ID are both serial fields in the base
-      // and revision table respectively, the revision ID is not known yet, when
-      // inserting data into the base table. Instead the revision ID in the base
-      // table is updated after the data has been inserted into the revision
-      // table. For this reason the revision ID field cannot be marked as NOT
-      // NULL.
-      unset($keys['label'], $keys['revision']);
-      // Key fields may not be NULL.
-      if (in_array($field_name, $keys)) {
-        $schema['fields'][$schema_field_name]['not null'] = TRUE;
+    foreach ($schema as $table_name => $table_schema) {
+      $table_schema = array_diff_key($table_schema, $unused_keys);
+      foreach ($keys as $key) {
+        // Exclude data generated from field storage definitions, we will check
+        // that separately.
+        if (!empty($table_schema[$key])) {
+          $data_keys = array_keys($table_schema[$key]);
+          $entity_keys = array_filter($data_keys, function ($key) use ($entity_type_id) {
+            return strpos($key, $entity_type_id . '_field__') !== 0;
+          });
+          $table_schema[$key] = array_intersect_key($table_schema[$key], array_flip($entity_keys));
+        }
       }
+      $schema_data[$table_name] = array_filter($table_schema);
     }
 
-    if (!empty($field_schema['indexes'])) {
-      $indexes = $this->getFieldIndexes($field_name, $field_schema, $column_mapping);
-      $schema['indexes'] = array_merge($schema['indexes'], $indexes);
-    }
-
-    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);
-    }
-
-    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);
-    }
+    return $schema_data;
   }
 
   /**
@@ -462,14 +661,14 @@ protected function getFieldSchemaData($field_name, array $field_schema, array $c
    *   The ID of the entity type.
    * @param string $field_name
    *   The name of the field.
-   * @param string $key
-   *   The key of the field.
+   * @param string|null $key
+   *   (optional) A further key to append to the name.
    *
    * @return string
    *   The field identifier name.
    */
-  protected function getFieldSchemaIdentifierName($entity_type_id, $field_name, $key) {
-    $real_key = "{$entity_type_id}_field__{$field_name}__{$key}";
+  protected function getFieldSchemaIdentifierName($entity_type_id, $field_name, $key = NULL) {
+    $real_key = isset($key) ? "{$entity_type_id}_field__{$field_name}__{$key}" : "{$entity_type_id}_field__{$field_name}";
     // Limit the string to 48 characters, keeping a 16 characters margin for db
     // prefixes.
     if (strlen($real_key) > 48) {
@@ -535,25 +734,99 @@ protected function addDefaultLangcodeSchema(&$schema) {
     );
   }
 
+  /**
+   * Loads stored schema data for the given entity type definition.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   *
+   * @return array
+   *   The entity schema data array.
+   */
+  protected function loadEntitySchemaData(EntityTypeInterface $entity_type) {
+    return $this->installedStorageSchema()->get($entity_type->id() . '.entity_schema_data', array());
+  }
+
+  /**
+   * Stores schema data for the given entity type definition.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param array $schema
+   *   The entity schema data array.
+   */
+  protected function saveEntitySchemaData(EntityTypeInterface $entity_type, $schema) {
+    $data = $this->getEntitySchemaData($entity_type, $schema);
+    $this->installedStorageSchema()->set($entity_type->id() . '.entity_schema_data', $data);
+  }
+
+  /**
+   * Deletes schema data for the given entity type definition.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   */
+  protected function deleteEntitySchemaData(EntityTypeInterface $entity_type) {
+    $this->installedStorageSchema()->delete($entity_type->id() . '.entity_schema_data');
+  }
+
+  /**
+   * Loads stored schema data for the given field storage definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   *
+   * @return array
+   *   The field schema data array.
+   */
+  protected function loadFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
+    return $this->installedStorageSchema()->get($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), array());
+  }
+
+  /**
+   * Stores schema data for the given field storage definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   * @param array $schema
+   *   The field schema data array.
+   */
+  protected function saveFieldSchemaData(FieldStorageDefinitionInterface $storage_definition, $schema) {
+    $this->installedStorageSchema()->set($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), $schema);
+  }
+
+  /**
+   * Deletes schema data for the given field storage definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   */
+  protected function deleteFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
+    $this->installedStorageSchema()->delete($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName());
+  }
+
   /**
    * Initializes common information for a base table.
    *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type.
+   *
    * @return array
    *   A partial schema array for the base table.
    */
-  protected function initializeBaseTable() {
-    $entity_type_id = $this->entityType->id();
+  protected function initializeBaseTable(ContentEntityTypeInterface $entity_type) {
+    $entity_type_id = $entity_type->id();
 
     $schema = array(
       'description' => "The base table for $entity_type_id entities.",
-      'primary key' => array($this->entityType->getKey('id')),
+      'primary key' => array($entity_type->getKey('id')),
       'indexes' => array(),
       'foreign keys' => array(),
     );
 
-    if ($this->entityType->hasKey('revision')) {
-      $revision_key = $this->entityType->getKey('revision');
-      $key_name = $this->getEntityIndexName($revision_key);
+    if ($entity_type->hasKey('revision')) {
+      $revision_key = $entity_type->getKey('revision');
+      $key_name = $this->getEntityIndexName($entity_type, $revision_key);
       $schema['unique keys'][$key_name] = array($revision_key);
       $schema['foreign keys'][$entity_type_id . '__revision'] = array(
         'table' => $this->storage->getRevisionTable(),
@@ -569,27 +842,30 @@ protected function initializeBaseTable() {
   /**
    * Initializes common information for a revision table.
    *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type.
+   *
    * @return array
    *   A partial schema array for the revision table.
    */
-  protected function initializeRevisionTable() {
-    $entity_type_id = $this->entityType->id();
-    $id_key = $this->entityType->getKey('id');
-    $revision_key = $this->entityType->getKey('revision');
+  protected function initializeRevisionTable(ContentEntityTypeInterface $entity_type) {
+    $entity_type_id = $entity_type->id();
+    $id_key = $entity_type->getKey('id');
+    $revision_key = $entity_type->getKey('revision');
 
     $schema = array(
       'description' => "The revision table for $entity_type_id entities.",
       'primary key' => array($revision_key),
       'indexes' => array(),
       'foreign keys' => array(
-         $entity_type_id . '__revisioned' => array(
+        $entity_type_id . '__revisioned' => array(
           'table' => $this->storage->getBaseTable(),
           'columns' => array($id_key => $id_key),
         ),
       ),
     );
 
-    $schema['indexes'][$this->getEntityIndexName($id_key)] = array($id_key);
+    $schema['indexes'][$this->getEntityIndexName($entity_type, $id_key)] = array($id_key);
 
     $this->addTableDefaults($schema);
 
@@ -599,12 +875,15 @@ protected function initializeRevisionTable() {
   /**
    * Initializes common information for a data table.
    *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type.
+   *
    * @return array
    *   A partial schema array for the data table.
    */
-  protected function initializeDataTable() {
-    $entity_type_id = $this->entityType->id();
-    $id_key = $this->entityType->getKey('id');
+  protected function initializeDataTable(ContentEntityTypeInterface $entity_type) {
+    $entity_type_id = $entity_type->id();
+    $id_key = $entity_type->getKey('id');
 
     $schema = array(
       'description' => "The data table for $entity_type_id entities.",
@@ -620,9 +899,9 @@ protected function initializeDataTable() {
       ),
     );
 
-    if ($this->entityType->hasKey('revision')) {
-      $key = $this->entityType->getKey('revision');
-      $schema['indexes'][$this->getEntityIndexName($key)] = array($key);
+    if ($entity_type->hasKey('revision')) {
+      $key = $entity_type->getKey('revision');
+      $schema['indexes'][$this->getEntityIndexName($entity_type, $key)] = array($key);
     }
 
     $this->addTableDefaults($schema);
@@ -633,13 +912,16 @@ protected function initializeDataTable() {
   /**
    * Initializes common information for a revision data table.
    *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type.
+   *
    * @return array
    *   A partial schema array for the revision data table.
    */
-  protected function initializeRevisionDataTable() {
-    $entity_type_id = $this->entityType->id();
-    $id_key = $this->entityType->getKey('id');
-    $revision_key = $this->entityType->getKey('revision');
+  protected function initializeRevisionDataTable(ContentEntityTypeInterface $entity_type) {
+    $entity_type_id = $entity_type->id();
+    $id_key = $entity_type->getKey('id');
+    $revision_key = $entity_type->getKey('revision');
 
     $schema = array(
       'description' => "The revision data table for $entity_type_id entities.",
@@ -682,51 +964,59 @@ protected function addTableDefaults(&$schema) {
   /**
    * Processes the gathered schema for a base table.
    *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type.
    * @param array $schema
    *   The table schema, passed by reference.
    *
    * @return array
    *   A partial schema array for the base table.
    */
-  protected function processBaseTable(array &$schema) {
-    $this->processIdentifierSchema($schema, $this->entityType->getKey('id'));
+  protected function processBaseTable(ContentEntityTypeInterface $entity_type, array &$schema) {
+    $this->processIdentifierSchema($schema, $entity_type->getKey('id'));
   }
 
   /**
    * Processes the gathered schema for a base table.
    *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type.
    * @param array $schema
    *   The table schema, passed by reference.
    *
    * @return array
    *   A partial schema array for the base table.
    */
-  protected function processRevisionTable(array &$schema) {
-    $this->processIdentifierSchema($schema, $this->entityType->getKey('revision'));
+  protected function processRevisionTable(ContentEntityTypeInterface $entity_type, array &$schema) {
+    $this->processIdentifierSchema($schema, $entity_type->getKey('revision'));
   }
 
   /**
    * Processes the gathered schema for a base table.
    *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type.
    * @param array $schema
    *   The table schema, passed by reference.
    *
    * @return array
    *   A partial schema array for the base table.
    */
-  protected function processDataTable(array &$schema) {
+  protected function processDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
   }
 
   /**
    * Processes the gathered schema for a base table.
    *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type.
    * @param array $schema
    *   The table schema, passed by reference.
    *
    * @return array
    *   A partial schema array for the base table.
    */
-  protected function processRevisionDataTable(array &$schema) {
+  protected function processRevisionDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
   }
 
   /**
@@ -744,19 +1034,665 @@ protected function processIdentifierSchema(&$schema, $key) {
     unset($schema['fields'][$key]['default']);
   }
 
+  /**
+   * 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 performFieldSchemaOperation($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);
+    }
+  }
+
+  /**
+   * 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);
+    }
+    $this->saveFieldSchemaData($storage_definition, $schema);
+  }
+
+  /**
+   * 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.
+   * @param bool $only_save
+   *   (optional) Whether to skip modification of database tables and only save
+   *   the schema data for future comparison. For internal use only. This is
+   *   used by onEntityTypeCreate() after it has already fully created the
+   *   shared tables.
+   */
+  protected function createSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, $only_save = FALSE) {
+    $created_field_name = $storage_definition->getName();
+    $table_mapping = $this->storage->getTableMapping();
+    $column_names = $table_mapping->getColumnNames($created_field_name);
+    $schema_handler = $this->database->schema();
+    $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
+
+    // Iterate over the mapped table to find the ones that will host the created
+    // field schema.
+    $schema = array();
+    foreach ($shared_table_names as $table_name) {
+      foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
+        if ($field_name == $created_field_name) {
+          // Create field columns.
+          $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
+          if (!$only_save) {
+            foreach ($schema[$table_name]['fields'] as $name => $specifier) {
+              $schema_handler->addField($table_name, $name, $specifier);
+            }
+            if (!empty($schema[$table_name]['indexes'])) {
+              foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
+                $schema_handler->addIndex($table_name, $name, $specifier);
+              }
+            }
+            if (!empty($schema[$table_name]['unique keys'])) {
+              foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
+                $schema_handler->addUniqueKey($table_name, $name, $specifier);
+              }
+            }
+          }
+          // After creating the field schema skip to the next table.
+          break;
+        }
+      }
+    }
+    $this->saveFieldSchemaData($storage_definition, $schema);
+  }
+
+  /**
+   * 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) {
+    // When switching from dedicated to shared field table layout we need need
+    // to delete the field tables with their regular names. When this happens
+    // original definitions will be defined.
+    $deleted = !$this->originalDefinitions;
+    $table_mapping = $this->storage->getTableMapping();
+    $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $deleted);
+    $this->database->schema()->dropTable($table_name);
+    if ($this->entityType->isRevisionable()) {
+      $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $deleted);
+      $this->database->schema()->dropTable($revision_name);
+    }
+    $this->deleteFieldSchemaData($storage_definition);
+  }
+
+  /**
+   * 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(
+      $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
+    );
+    $column_names = $table_mapping->getColumnNames($deleted_field_name);
+    $schema_handler = $this->database->schema();
+    $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
+
+    // Iterate over the mapped table to find the ones that host the deleted
+    // field schema.
+    foreach ($shared_table_names as $table_name) {
+      foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
+        if ($field_name == $deleted_field_name) {
+          $schema = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
+
+          // 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;
+        }
+      }
+    }
+
+    $this->deleteFieldSchemaData($storage_definition);
+  }
+
+  /**
+   * 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.
+   * @throws \Exception
+   *   Rethrown exception if the table recreation fails.
+   */
+  protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    if (!$this->storage->countFieldData($original, TRUE)) {
+      // 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 {
+        // Since there is no data we may be switching from a shared table schema
+        // to a dedicated table schema, hence we should use the proper API.
+        $this->performFieldSchemaOperation('delete', $original);
+        $this->performFieldSchemaOperation('create', $storage_definition);
+      }
+      catch (\Exception $e) {
+        if ($this->database->supportsTransactionalDDL()) {
+          $transaction->rollback();
+        }
+        else {
+          // Recreate tables.
+          $this->performFieldSchemaOperation('create', $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.");
+      }
+      // 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);
+        }
+      }
+      $this->saveFieldSchemaData($storage_definition, $this->getDedicatedTableSchema($storage_definition));
+    }
+  }
+
+  /**
+   * 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.
+   * @throws \Exception
+   *   Rethrown exception if the table recreation fails.
+   */
+  protected function updateSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    if (!$this->storage->countFieldData($original, 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 {
+        // Since there is no data we may be switching from a dedicated table
+        // to a schema table schema, hence we should use the proper API.
+        $this->performFieldSchemaOperation('delete', $original);
+        $this->performFieldSchemaOperation('create', $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.");
+      }
+
+      $updated_field_name = $storage_definition->getName();
+      $table_mapping = $this->storage->getTableMapping();
+      $column_names = $table_mapping->getColumnNames($updated_field_name);
+      $schema_handler = $this->database->schema();
+
+      // Iterate over the mapped table to find the ones that host the deleted
+      // field schema.
+      $original_schema = $this->loadFieldSchemaData($original);
+      $schema = array();
+      foreach ($table_mapping->getTableNames() as $table_name) {
+        foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
+          if ($field_name == $updated_field_name) {
+            $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
+
+            // Drop original indexes and unique keys.
+            if (!empty($original_schema[$table_name]['indexes'])) {
+              foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) {
+                $schema_handler->dropIndex($table_name, $name);
+              }
+            }
+            if (!empty($original_schema[$table_name]['unique keys'])) {
+              foreach ($original_schema[$table_name]['unique keys'] as $name => $specifier) {
+                $schema_handler->dropUniqueKey($table_name, $name);
+              }
+            }
+            // Create new indexes and unique keys.
+            if (!empty($schema[$table_name]['indexes'])) {
+              foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
+                $schema_handler->addIndex($table_name, $name, $specifier);
+              }
+            }
+            if (!empty($schema[$table_name]['unique keys'])) {
+              foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
+                $schema_handler->addUniqueKey($table_name, $name, $specifier);
+              }
+            }
+            // After deleting the field schema skip to the next table.
+            break;
+          }
+        }
+      }
+      $this->saveFieldSchemaData($storage_definition, $schema);
+    }
+  }
+
+  /**
+   * Returns the schema for a single field definition.
+   *
+   * Entity types may override this method in order to optimize the generated
+   * schema for given field. While all optimizations that apply to a single
+   * field have to be added here, all cross-field optimizations should be via
+   * SqlContentEntityStorageSchema::getEntitySchema() instead; e.g.,
+   * an index spanning multiple fields.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field whose schema has to be returned.
+   * @param string $table_name
+   *   The name of the table columns will be added to.
+   * @param string[] $column_mapping
+   *   A mapping of field column names to database column names.
+   *
+   * @return array
+   *   The schema definition for the table with the following keys:
+   *   - fields: The schema definition for the each field columns.
+   *   - indexes: The schema definition for the indexes.
+   *   - unique keys: The schema definition for the unique keys.
+   *   - foreign keys: The schema definition for the foreign keys.
+   *
+   * @throws \Drupal\Core\Field\FieldException
+   *   Exception thrown if the schema contains reserved column names.
+   */
+  protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, 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(format_string('Illegal field column names on @field_name', array('@field_name' => $storage_definition->getName())));
+    }
+
+    $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];
+
+      $schema['fields'][$schema_field_name] = $column_schema;
+      $schema['fields'][$schema_field_name]['description'] = $field_description;
+      // Only entity keys are required.
+      $keys = $this->entityType->getKeys() + array('langcode' => 'langcode');
+      // The label is an entity key, but label fields are not necessarily
+      // required.
+      // Because entity ID and revision ID are both serial fields in the base
+      // and revision table respectively, the revision ID is not known yet, when
+      // inserting data into the base table. Instead the revision ID in the base
+      // table is updated after the data has been inserted into the revision
+      // table. For this reason the revision ID field cannot be marked as NOT
+      // NULL.
+      unset($keys['label'], $keys['revision']);
+      // Key fields may not be NULL.
+      if (in_array($field_name, $keys)) {
+        $schema['fields'][$schema_field_name]['not null'] = TRUE;
+      }
+    }
+
+    if (!empty($field_schema['indexes'])) {
+      $schema['indexes'] = $this->getFieldIndexes($field_name, $field_schema, $column_mapping);
+    }
+
+    if (!empty($field_schema['unique keys'])) {
+      $schema['unique keys'] = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping);
+    }
+
+    if (!empty($field_schema['foreign keys'])) {
+      $schema['foreign keys'] = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping);
+    }
+
+    return $schema;
+  }
+
+  /**
+   * Adds an index for the specified field to the given schema definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field for which an index should be added.
+   * @param array $schema
+   *   A reference to the schema array to be updated.
+   * @param bool $not_null
+   *   (optional) Whether to also add a 'not null' constraint to the column
+   *   being indexed. Doing so improves index performance. Defaults to FALSE,
+   *   in case the field needs to support NULL values.
+   * @param int $size
+   *   (optional) The index size. Defaults to no limit.
+   */
+  protected function addSharedTableFieldIndex(FieldStorageDefinitionInterface $storage_definition, &$schema, $not_null = FALSE, $size = NULL) {
+    $name = $storage_definition->getName();
+    $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
+    $schema['indexes'][$real_key] = array($size ? array($name, $size) : $name);
+    if ($not_null) {
+      $schema['fields'][$name]['not null'] = TRUE;
+    }
+  }
+
+  /**
+   * Adds a unique key for the specified field to the given schema definition.
+   *
+   * Also adds a 'not null' constraint, because many databases do not reliably
+   * support unique keys on null columns.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field to which to add a unique key.
+   * @param array $schema
+   *   A reference to the schema array to be updated.
+   */
+  protected function addSharedTableFieldUniqueKey(FieldStorageDefinitionInterface $storage_definition, &$schema) {
+    $name = $storage_definition->getName();
+    $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
+    $schema['unique keys'][$real_key] = array($name);
+    $schema['fields'][$name]['not null'] = TRUE;
+  }
+
+  /**
+   * Adds a foreign key for the specified field to the given schema definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field to which to add a foreign key.
+   * @param array $schema
+   *   A reference to the schema array to be updated.
+   * @param string $foreign_table
+   *   The foreign table.
+   * @param string $foreign_column
+   *   The foreign column.
+   */
+  protected function addSharedTableFieldForeignKey(FieldStorageDefinitionInterface $storage_definition, &$schema, $foreign_table, $foreign_column) {
+    $name = $storage_definition->getName();
+    $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
+    $schema['foreign keys'][$real_key] = array(
+      'table' => $foreign_table,
+      'columns' => array($name => $foreign_column),
+    );
+  }
+
+  /**
+   * Returns the SQL schema for a dedicated table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   (optional) The entity type definition. Defaults to the one returned by
+   *   the entity manager.
+   *
+   * @return array
+   *   The schema definition for the table with the following keys:
+   *   - fields: The schema definition for the each field columns.
+   *   - indexes: The schema definition for the indexes.
+   *   - unique keys: The schema definition for the unique keys.
+   *   - foreign keys: The schema definition for the foreign keys.
+   *
+   * @throws \Drupal\Core\Field\FieldException
+   *   Exception thrown if the schema contains reserved column names.
+   *
+   * @see hook_schema()
+   */
+  protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, ContentEntityTypeInterface $entity_type = NULL) {
+    $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.
+    if (!$this->entityType->isRevisionable()) {
+      $revision_id_schema = $id_schema;
+      $revision_id_schema['description'] = 'The entity revision id this data is attached to, which for an unversioned entity type is the same as the entity id';
+    }
+    elseif ($this->fieldStorageDefinitions[$this->entityType->getKey('revision')]->getType() == 'integer') {
+      $revision_id_schema = array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'description' => 'The entity revision id this data is attached to',
+      );
+    }
+    else {
+      $revision_id_schema = array(
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => TRUE,
+        'description' => 'The entity revision id this data is attached to',
+      );
+    }
+
+    $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'),
+      ),
+    );
+
+    // Check that the schema does not include forbidden column names.
+    $schema = $storage_definition->getSchema();
+    $table_mapping = $this->storage->getTableMapping();
+    if (array_intersect(array_keys($schema['columns']), $table_mapping->getReservedColumns())) {
+      throw new FieldException(format_string('Illegal field column names on @field_name', array('@field_name' => $storage_definition->getName())));
+    }
+
+    // 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;
+      }
+    }
+
+    $dedicated_table_schema = array($table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema);
+
+    // If the entity type is revisionable, construct the revision table.
+    $entity_type = $entity_type ?: $this->entityType;
+    if ($entity_type->isRevisionable()) {
+      $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';
+      $dedicated_table_schema += array($table_mapping->getDedicatedRevisionTableName($storage_definition) => $revision_schema);
+    }
+
+    return $dedicated_table_schema;
+  }
+
   /**
    * Returns the name to be used for the given entity index.
    *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type.
    * @param string $index
    *   The index column name.
    *
    * @return string
    *   The index name.
    */
-  protected function getEntityIndexName($index) {
-    return $this->entityType->id() . '__' . $index;
+  protected function getEntityIndexName(ContentEntityTypeInterface $entity_type, $index) {
+    return $entity_type->id() . '__' . $index;
   }
 
+  /**
+   * Generates an index name for a field data table.
+   *
+   * @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;
+  }
   /**
    * Checks whether a database table is non-existent or empty.
    *
@@ -768,7 +1704,7 @@ protected function getEntityIndexName($index) {
    * @return bool
    *   TRUE if the table is empty, FALSE otherwise.
    */
-  protected function tableIsEmpty($table_name) {
+  protected function isTableEmpty($table_name) {
     return !$this->database->schema()->tableExists($table_name) ||
       !$this->database->select($table_name)
         ->countQuery()
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php
index b0a9b05c4a1fd871fc09a3ceb0333e98a01fc771..984d05e3c344888e10f860e46ea082e105fa5a3d 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php
@@ -17,9 +17,13 @@ interface SqlEntityStorageInterface extends EntityStorageInterface {
   /**
    * Gets a table mapping for the entity's SQL tables.
    *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
+   *   (optional) An array of field storage definitions to be used to compute
+   *   the table mapping. Defaults to the ones provided by the entity manager.
+   *
    * @return \Drupal\Core\Entity\Sql\TableMappingInterface
    *   A table mapping object for the entity's tables.
    */
-  public function getTableMapping();
+  public function getTableMapping(array $storage_definitions = NULL);
 
 }
diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php
index 0917e42b72cbb1ab4169ef22243ada3f3a15470a..92e974853b1c60dcae2331b850ccddac991fe9c5 100644
--- a/core/lib/Drupal/Core/Extension/ModuleHandler.php
+++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
@@ -821,6 +821,17 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
           $version = max(max($versions), $version);
         }
 
+        // Notify the entity manager that this module's entity types are new,
+        // so that it can notify all interested handlers. For example, a
+        // SQL-based storage handler can use this as an opportunity to create
+        // the necessary database tables.
+        $entity_manager = \Drupal::entityManager();
+        foreach ($entity_manager->getDefinitions() as $entity_type) {
+          if ($entity_type->getProvider() == $module) {
+            $entity_manager->onEntityTypeCreate($entity_type);
+          }
+        }
+
         // Install default configuration of the module.
         $config_installer = \Drupal::service('config.installer');
         if ($sync_status) {
@@ -844,17 +855,6 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
         }
         drupal_set_installed_schema_version($module, $version);
 
-        // Notify the entity manager that this module's entity types are new,
-        // so that it can notify all interested handlers. For example, a
-        // SQL-based storage handler can use this as an opportunity to create
-        // the necessary database tables.
-        $entity_manager = \Drupal::entityManager();
-        foreach ($entity_manager->getDefinitions() as $entity_type) {
-          if ($entity_type->getProvider() == $module) {
-            $entity_manager->onEntityTypeCreate($entity_type);
-          }
-        }
-
         // Record the fact that it was installed.
         $modules_installed[] = $module;
 
diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
index 20b31ad5a3cbe95dca7ad2045fbd1d8d873d4426..efddd231657ff5229872953181177c466dd3ee23 100644
--- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
+++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
@@ -553,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'];
@@ -583,19 +578,17 @@ public function getColumns() {
   }
 
   /**
-   * A list of columns that can not be used as field type columns.
-   *
-   * @return array
+   * {@inheritdoc}
    */
-  public static function getReservedColumns() {
-    return array('deleted');
+  public function hasCustomStorage() {
+    return !empty($this->definition['custom_storage']) || $this->isComputed();
   }
 
   /**
    * {@inheritdoc}
    */
-  public function hasCustomStorage() {
-    return !empty($this->definition['custom_storage']) || $this->isComputed();
+  public function isBaseField() {
+    return TRUE;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php b/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php
index 10f090e1d074cbc053039191da3cc6d1e22f26f7..6ace3fa9f916af17f26f35236980cde6cf0c7cfe 100644
--- a/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php
+++ b/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php
@@ -297,6 +297,18 @@ public function getProvider();
    */
   public function hasCustomStorage();
 
+  /**
+   * Determines whether the field is a base field.
+   *
+   * Base fields are not specific to a given bundle or a set of bundles. This
+   * excludes configurable fields, as they are always attached to a specific
+   * bundle.
+   *
+   * @return bool
+   *   Whether the field is a base field.
+   */
+  public function isBaseField();
+
   /**
    * Returns a unique identifier for the field.
    *
diff --git a/core/modules/aggregator/src/FeedStorageSchema.php b/core/modules/aggregator/src/FeedStorageSchema.php
index bb197bcf5dd123ad5714a6b18e712a5cdd714c48..d251ed5f1ea019e04ba915c79f20107978a6d041 100644
--- a/core/modules/aggregator/src/FeedStorageSchema.php
+++ b/core/modules/aggregator/src/FeedStorageSchema.php
@@ -7,8 +7,8 @@
 
 namespace Drupal\aggregator;
 
-use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * Defines the feed schema handler.
@@ -18,22 +18,25 @@ class FeedStorageSchema extends SqlContentEntityStorageSchema {
   /**
    * {@inheritdoc}
    */
-  protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
-    $schema = parent::getEntitySchema($entity_type, $reset);
-
-    // Marking the respective fields as NOT NULL makes the indexes more
-    // performant.
-    $schema['aggregator_feed']['fields']['url']['not null'] = TRUE;
-    $schema['aggregator_feed']['fields']['queued']['not null'] = TRUE;
-    $schema['aggregator_feed']['fields']['title']['not null'] = TRUE;
-
-    $schema['aggregator_feed']['indexes'] += array(
-      'aggregator_feed__url'  => array(array('url', 255)),
-      'aggregator_feed__queued' => array('queued'),
-    );
-    $schema['aggregator_feed']['unique keys'] += array(
-      'aggregator_feed__title' => array('title'),
-    );
+  protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
+    $schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
+    $field_name = $storage_definition->getName();
+
+    if ($table_name == 'aggregator_feed') {
+      switch ($field_name) {
+        case 'url':
+          $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE, 255);
+          break;
+
+        case 'queued':
+          $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
+          break;
+
+        case 'title':
+          $this->addSharedTableFieldUniqueKey($storage_definition, $schema);
+          break;
+      }
+    }
 
     return $schema;
   }
diff --git a/core/modules/aggregator/src/ItemStorageSchema.php b/core/modules/aggregator/src/ItemStorageSchema.php
index ae9af8439c715f5b3311bc93bc3cfb75963df0b0..9688f670bd797cab3bafd9e033016d9649d1c6be 100644
--- a/core/modules/aggregator/src/ItemStorageSchema.php
+++ b/core/modules/aggregator/src/ItemStorageSchema.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * Defines the item schema handler.
@@ -18,22 +19,21 @@ class ItemStorageSchema extends SqlContentEntityStorageSchema {
   /**
    * {@inheritdoc}
    */
-  protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
-    $schema = parent::getEntitySchema($entity_type, $reset);
-
-    // Marking the respective fields as NOT NULL makes the indexes more
-    // performant.
-    $schema['aggregator_item']['fields']['timestamp']['not null'] = TRUE;
-
-    $schema['aggregator_item']['indexes'] += array(
-      'aggregator_item__timestamp' => array('timestamp'),
-    );
-    $schema['aggregator_item']['foreign keys'] += array(
-      'aggregator_item__aggregator_feed' => array(
-        'table' => 'aggregator_feed',
-        'columns' => array('fid' => 'fid'),
-      ),
-    );
+  protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
+    $schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
+    $field_name = $storage_definition->getName();
+
+    if ($table_name == 'aggregator_item') {
+      switch ($field_name) {
+        case 'timestamp':
+          $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
+          break;
+
+        case 'fid':
+          $this->addSharedTableFieldForeignKey($storage_definition, $schema, 'aggregator_feed', 'fid');
+          break;
+      }
+    }
 
     return $schema;
   }
diff --git a/core/modules/block_content/src/BlockContentStorageSchema.php b/core/modules/block_content/src/BlockContentStorageSchema.php
index 18295d31348a991e5902c28ff9c6cbe3a2e6856f..c1674aef125d616d078ff028d3e93eb9987103b5 100644
--- a/core/modules/block_content/src/BlockContentStorageSchema.php
+++ b/core/modules/block_content/src/BlockContentStorageSchema.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * Defines the block content schema handler.
@@ -21,10 +22,6 @@ class BlockContentStorageSchema extends SqlContentEntityStorageSchema {
   protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
     $schema = parent::getEntitySchema($entity_type, $reset);
 
-    // Marking the respective fields as NOT NULL makes the indexes more
-    // performant.
-    $schema['block_content_field_data']['fields']['info']['not null'] = TRUE;
-
     $schema['block_content_field_data']['unique keys'] += array(
       'block_content__info' => array('info', 'langcode'),
     );
@@ -32,4 +29,24 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
     return $schema;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
+    $schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
+    $field_name = $storage_definition->getName();
+
+    if ($table_name == 'block_content_field_data') {
+      switch ($field_name) {
+        case 'info':
+          // Improves the performance of the block_content__info index defined
+          // in getEntitySchema().
+          $schema['fields'][$field_name]['not null'] = TRUE;
+          break;
+      }
+    }
+
+    return $schema;
+  }
+
 }
diff --git a/core/modules/comment/src/CommentStorageSchema.php b/core/modules/comment/src/CommentStorageSchema.php
index 7b9a03c397312be1cb1a16b232d0d4d68995876d..9196f9eb82084d8ddbb3ca3a861252a9541d5d0f 100644
--- a/core/modules/comment/src/CommentStorageSchema.php
+++ b/core/modules/comment/src/CommentStorageSchema.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * Defines the comment schema handler.
@@ -21,13 +22,6 @@ class CommentStorageSchema extends SqlContentEntityStorageSchema {
   protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
     $schema = parent::getEntitySchema($entity_type, $reset);
 
-    // Marking the respective fields as NOT NULL makes the indexes more
-    // performant.
-    $schema['comment_field_data']['fields']['created']['not null'] = TRUE;
-    $schema['comment_field_data']['fields']['thread']['not null'] = TRUE;
-
-    unset($schema['comment_field_data']['indexes']['comment_field__pid__target_id']);
-    unset($schema['comment_field_data']['indexes']['comment_field__entity_id__target_id']);
     $schema['comment_field_data']['indexes'] += array(
       'comment__status_pid' => array('pid', 'status'),
       'comment__num_new' => array(
@@ -45,16 +39,40 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
         'comment_type',
         'default_langcode',
       ),
-      'comment__created' => array('created'),
-    );
-    $schema['comment_field_data']['foreign keys'] += array(
-      'comment__author' => array(
-        'table' => 'users',
-        'columns' => array('uid' => 'uid'),
-      ),
     );
 
     return $schema;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
+    $schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
+    $field_name = $storage_definition->getName();
+
+    if ($table_name == 'comment_field_data') {
+      // Remove unneeded indexes.
+      unset($schema['indexes']['comment_field__pid__target_id']);
+      unset($schema['indexes']['comment_field__entity_id__target_id']);
+
+      switch ($field_name) {
+        case 'thread':
+          // Improves the performance of the comment__num_new index defined
+          // in getEntitySchema().
+          $schema['fields'][$field_name]['not null'] = TRUE;
+          break;
+
+        case 'created':
+          $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
+          break;
+
+        case 'uid':
+          $this->addSharedTableFieldForeignKey($storage_definition, $schema, 'users', 'uid');
+      }
+    }
+
+    return $schema;
+  }
+
 }
diff --git a/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.module b/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.module
index 70311f13f3aa8a127747d0c90e4d279998a44cdf..116a326bbba9c7e15c35230e1b56ff8b37134947 100644
--- a/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.module
+++ b/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.module
@@ -13,15 +13,11 @@
 function contact_storage_test_entity_base_field_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type) {
   if ($entity_type->id() == 'contact_message') {
     $fields = array();
+
     $fields['id'] = BaseFieldDefinition::create('integer')
       ->setLabel(t('Message ID'))
       ->setDescription(t('The message ID.'))
       ->setReadOnly(TRUE)
-      // Explicitly set this to 'contact' so that
-      // SqlContentEntityStorage::usesDedicatedTable() doesn't attempt to
-      // put the ID in a dedicated table.
-      // @todo Remove when https://www.drupal.org/node/1498720 is in.
-      ->setProvider('contact')
       ->setSetting('unsigned', TRUE);
 
     return $fields;
diff --git a/core/modules/field/src/Entity/FieldStorageConfig.php b/core/modules/field/src/Entity/FieldStorageConfig.php
index 128ac014a0ecc0e2ff0043b553c89abf5e9437f1..4f346c5775a151abead00cfb07d084a87713cfc9 100644
--- a/core/modules/field/src/Entity/FieldStorageConfig.php
+++ b/core/modules/field/src/Entity/FieldStorageConfig.php
@@ -424,11 +424,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'];
@@ -446,6 +441,13 @@ public function hasCustomStorage() {
     return FALSE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isBaseField() {
+    return FALSE;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -607,15 +609,6 @@ public function isQueryable() {
     return TRUE;
   }
 
-  /**
-   * 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.
    *
diff --git a/core/modules/file/src/FileStorageSchema.php b/core/modules/file/src/FileStorageSchema.php
index f223e7b66020520641eec59587440eb56059699f..9c66e66aab9e46ea153f98a6dfb815072255b3a8 100644
--- a/core/modules/file/src/FileStorageSchema.php
+++ b/core/modules/file/src/FileStorageSchema.php
@@ -7,8 +7,8 @@
 
 namespace Drupal\file;
 
-use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * Defines the file schema handler.
@@ -18,24 +18,25 @@ class FileStorageSchema extends SqlContentEntityStorageSchema {
   /**
    * {@inheritdoc}
    */
-  protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
-    $schema = parent::getEntitySchema($entity_type, $reset);
-
-    // Marking the respective fields as NOT NULL makes the indexes more
-    // performant.
-    $schema['file_managed']['fields']['status']['not null'] = TRUE;
-    $schema['file_managed']['fields']['changed']['not null'] = TRUE;
-    $schema['file_managed']['fields']['uri']['not null'] = TRUE;
-
-    // @todo There should be a 'binary' field type or setting.
-    $schema['file_managed']['fields']['uri']['binary'] = TRUE;
-    $schema['file_managed']['indexes'] += array(
-      'file__status' => array('status'),
-      'file__changed' => array('changed'),
-    );
-    $schema['file_managed']['unique keys'] += array(
-      'file__uri' => array('uri'),
-    );
+  protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
+    $schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
+    $field_name = $storage_definition->getName();
+
+    if ($table_name == 'file_managed') {
+      switch ($field_name) {
+        case 'status':
+        case 'changed':
+          $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
+          break;
+
+        case 'uri':
+          $this->addSharedTableFieldUniqueKey($storage_definition, $schema, TRUE);
+          // @todo There should be a 'binary' field type or setting:
+          //   https://www.drupal.org/node/2068655.
+          $schema['fields'][$field_name]['binary'] = TRUE;
+          break;
+      }
+    }
 
     return $schema;
   }
diff --git a/core/modules/node/src/NodeStorageSchema.php b/core/modules/node/src/NodeStorageSchema.php
index 4bd15c0f2d2eba4dfcd876e4b50583d2c334b4fd..4d02d3b0d82541abcceebc923b0654fe7efc367a 100644
--- a/core/modules/node/src/NodeStorageSchema.php
+++ b/core/modules/node/src/NodeStorageSchema.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * Defines the node schema handler.
@@ -23,31 +24,11 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
 
     // Marking the respective fields as NOT NULL makes the indexes more
     // performant.
-    $schema['node_field_data']['fields']['changed']['not null'] = TRUE;
-    $schema['node_field_data']['fields']['created']['not null'] = TRUE;
     $schema['node_field_data']['fields']['default_langcode']['not null'] = TRUE;
-    $schema['node_field_data']['fields']['promote']['not null'] = TRUE;
-    $schema['node_field_data']['fields']['status']['not null'] = TRUE;
-    $schema['node_field_data']['fields']['sticky']['not null'] = TRUE;
-    $schema['node_field_data']['fields']['title']['not null'] = TRUE;
     $schema['node_field_revision']['fields']['default_langcode']['not null'] = TRUE;
 
-    // @todo Revisit index definitions in https://drupal.org/node/2015277.
-    $schema['node_revision']['indexes'] += array(
-      'node__langcode' => array('langcode'),
-    );
-    $schema['node_revision']['foreign keys'] += array(
-      'node__revision_author' => array(
-        'table' => 'users',
-        'columns' => array('revision_uid' => 'uid'),
-      ),
-    );
-
     $schema['node_field_data']['indexes'] += array(
-      'node__changed' => array('changed'),
-      'node__created' => array('created'),
       'node__default_langcode' => array('default_langcode'),
-      'node__langcode' => array('langcode'),
       'node__frontpage' => array('promote', 'status', 'sticky', 'created'),
       'node__status_type' => array('status', 'type', 'nid'),
       'node__title_type' => array('title', array('type', 4)),
@@ -55,10 +36,59 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
 
     $schema['node_field_revision']['indexes'] += array(
       'node__default_langcode' => array('default_langcode'),
-      'node__langcode' => array('langcode'),
     );
 
     return $schema;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
+    $schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
+    $field_name = $storage_definition->getName();
+
+    if ($table_name == 'node_revision') {
+      switch ($field_name) {
+        case 'langcode':
+          $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
+          break;
+
+        case 'revision_uid':
+          $this->addSharedTableFieldForeignKey($storage_definition, $schema, 'users', 'uid');
+          break;
+      }
+    }
+
+    if ($table_name == 'node_field_data') {
+      switch ($field_name) {
+        case 'promote':
+        case 'status':
+        case 'sticky':
+        case 'title':
+          // Improves the performance of the indexes defined
+          // in getEntitySchema().
+          $schema['fields'][$field_name]['not null'] = TRUE;
+          break;
+
+        case 'changed':
+        case 'created':
+        case 'langcode':
+          // @todo Revisit index definitions: https://drupal.org/node/2015277.
+          $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
+          break;
+      }
+    }
+
+    if ($table_name == 'node_field_revision') {
+      switch ($field_name) {
+        case 'langcode':
+          $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
+          break;
+      }
+    }
+
+    return $schema;
+  }
+
 }
diff --git a/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php b/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php
index 755527b6694e6650bbc410239bc31a0f9d5cb9db..0866f7ae8db7391f59f7611e58c05b06e8075744 100644
--- a/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php
+++ b/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php
@@ -14,6 +14,13 @@
  */
 class EntityBundleFieldTest extends EntityUnitTestBase  {
 
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('entity_schema_test');
+
   /**
    * The module handler.
    *
@@ -39,59 +46,42 @@ protected function setUp() {
     $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\DefaultTableMapping $table_mapping */
-    $table_mapping = $this->entityManager->getStorage('entity_test')->getTableMapping();
-    $table = $table_mapping->getDedicatedDataTableName($definition);
-    $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() {
+    entity_test_create_bundle('custom');
+
     // 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'));
+    $this->assertFalse($entity->hasField('custom_bundle_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';
+    $this->assertTrue($entity->hasField('custom_bundle_field'));
+    $entity->custom_bundle_field->value = 'swanky';
     $entity->save();
     $storage->resetCache();
     $entity = $storage->load($entity->id());
-    $this->assertEqual($entity->custom_field->value, 'swanky', 'Entity was saved correct.y');
+    $this->assertEqual($entity->custom_bundle_field->value, 'swanky', 'Entity was saved correctly');
 
-    $entity->custom_field->value = 'cozy';
+    $entity->custom_bundle_field->value = 'cozy';
     $entity->save();
     $storage->resetCache();
     $entity = $storage->load($entity->id());
-    $this->assertEqual($entity->custom_field->value, 'cozy', 'Entity was updated correctly.');
+    $this->assertEqual($entity->custom_bundle_field->value, 'cozy', 'Entity was updated correctly.');
 
     $entity->delete();
     /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
     $table_mapping = $storage->getTableMapping();
-    $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_field'));
+    $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_bundle_field'));
     $result = $this->database->select($table, 'f')
       ->fields('f')
       ->condition('f.entity_id', $entity->id())
@@ -100,11 +90,11 @@ public function testCustomBundleFieldUsage() {
 
     // 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 = $storage->create(['type' => 'custom', 'custom_bundle_field' => 'new']);
     $entity->save();
     entity_test_delete_bundle('custom');
 
-    $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_field'));
+    $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_bundle_field'));
     $result = $this->database->select($table, 'f')
       ->condition('f.entity_id', $entity->id())
       ->condition('deleted', 1)
@@ -112,7 +102,8 @@ public function testCustomBundleFieldUsage() {
       ->execute();
     $this->assertEqual(1, $result->fetchField(), 'Field data has been deleted');
 
-    // @todo Test field purge and table deletion once supported.
+    // @todo Test field purge and table deletion once supported. See
+    //   https://www.drupal.org/node/2282119.
     // $this->assertFalse($this->database->schema()->tableExists($table), 'Custom field table was deleted');
   }
 
diff --git a/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
index 655aadb10900ebccbe4bee7b81f8507683f94ae9..752a1f36cf023cde71d247fc74f31b29a510e6eb 100644
--- a/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
+++ b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
@@ -8,6 +8,9 @@
 namespace Drupal\system\Tests\Entity;
 
 use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\entity_test\FieldStorageDefinition;
 
 /**
  * Tests EntityDefinitionUpdateManager functionality.
@@ -37,17 +40,18 @@ protected function setUp() {
     parent::setUp();
     $this->entityDefinitionUpdateManager = $this->container->get('entity.definition_update_manager');
     $this->database = $this->container->get('database');
+
+    // Install every entity type's schema that wasn't installed in the parent
+    // method.
+    foreach (array_diff_key($this->entityManager->getDefinitions(), array_flip(array('user', 'entity_test'))) as $entity_type_id => $entity_type) {
+      $this->installEntitySchema($entity_type_id);
+    }
   }
 
   /**
    * Tests when no definition update is needed.
    */
   public function testNoUpdates() {
-    // Install every entity type's schema.
-    foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
-      $this->installEntitySchema($entity_type_id);
-    }
-
     // Ensure that the definition update manager reports no updates.
     $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that no updates are needed.');
     $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), array(), 'EntityDefinitionUpdateManager reports an empty change summary.');
@@ -60,50 +64,375 @@ public function testNoUpdates() {
   /**
    * Tests updating entity schema when there are no existing entities.
    */
-  public function testUpdateWithoutData() {
-    // Install every entity type's schema. Start with entity_test_rev not
-    // supporting revisions, and ensure its revision table isn't created.
-    $this->state->set('entity_test.entity_test_rev.disable_revisable', TRUE);
-    $this->entityManager->clearCachedDefinitions();
-    foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
-      $this->installEntitySchema($entity_type_id);
-    }
-    $this->assertFalse($this->database->schema()->tableExists('entity_test_rev_revision'), 'Revision table not created for entity_test_rev.');
+  public function testEntityTypeUpdateWithoutData() {
+    // The 'entity_test_update' entity type starts out non-revisionable, so
+    // ensure the revision table hasn't been created during setUp().
+    $this->assertFalse($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table not created for entity_test_update.');
 
-    // Restore entity_test_rev back to supporting revisions and ensure the
-    // definition update manager reports that an update is needed.
-    $this->state->delete('entity_test.entity_test_rev.disable_revisable');
+    // Update it to be revisionable and ensure the definition update manager
+    // reports that an update is needed.
+    $this->updateEntityTypeToRevisionable();
     $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
     $expected = array(
-      'entity_test_rev' => array(
-        t('Update the %entity_type entity type.', array('%entity_type' => $this->entityManager->getDefinition('entity_test_rev')->getLabel())),
+      'entity_test_update' => array(
+        t('Update the %entity_type entity type.', array('%entity_type' => $this->entityManager->getDefinition('entity_test_update')->getLabel())),
       ),
     );
-    $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
+    $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), $expected); //, 'EntityDefinitionUpdateManager reports the expected change summary.');
 
     // Run the update and ensure the revision table is created.
     $this->entityDefinitionUpdateManager->applyUpdates();
-    $this->assertTrue($this->database->schema()->tableExists('entity_test_rev_revision'), 'Revision table created for entity_test_rev.');
+    $this->assertTrue($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table created for entity_test_update.');
   }
 
   /**
    * Tests updating entity schema when there are existing entities.
    */
-  public function testUpdateWithData() {
-    // Install every entity type's schema. Start with entity_test_rev not
-    // supporting revisions.
-    $this->state->set('entity_test.entity_test_rev.disable_revisable', TRUE);
-    $this->entityManager->clearCachedDefinitions();
-    foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
-      $this->installEntitySchema($entity_type_id);
+  public function testEntityTypeUpdateWithData() {
+    // Save an entity.
+    $this->entityManager->getStorage('entity_test_update')->create()->save();
+
+    // Update the entity type to be revisionable and try to apply the update.
+    // It's expected to throw an exception.
+    $this->updateEntityTypeToRevisionable();
+    try {
+      $this->entityDefinitionUpdateManager->applyUpdates();
+      $this->fail('EntityStorageException thrown when trying to apply an update that requires data migration.');
     }
+    catch (EntityStorageException $e) {
+      $this->pass('EntityStorageException thrown when trying to apply an update that requires data migration.');
+    }
+  }
 
+  /**
+   * Tests creating, updating, and deleting a base field if no entities exist.
+   */
+  public function testBaseFieldCreateUpdateDeleteWithoutData() {
+    // Add a base field, ensure the update manager reports it, and the update
+    // creates its schema.
+    $this->addBaseField();
+    $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+    $expected = array(
+      'entity_test_update' => array(
+        t('Create the %field_name field.', array('%field_name' => t('A new base field'))),
+      ),
+    );
+    $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.');
+
+    // Add an index on the base field, ensure the update manager reports it,
+    // and the update creates it.
+    $this->addBaseFieldIndex();
+    $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+    $expected = array(
+      'entity_test_update' => array(
+        t('Update the %field_name field.', array('%field_name' => t('A new base field'))),
+      ),
+    );
+    $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index created.');
+
+    // Remove the above index, ensure the update manager reports it, and the
+    // update deletes it.
+    $this->removeBaseFieldIndex();
+    $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+    $expected = array(
+      'entity_test_update' => array(
+        t('Update the %field_name field.', array('%field_name' => t('A new base field'))),
+      ),
+    );
+    $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index deleted.');
+
+    // Update the type of the base field from 'string' to 'text', ensure the
+    // update manager reports it, and the update adjusts the schema
+    // accordingly.
+    $this->modifyBaseField();
+    $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+    $expected = array(
+      'entity_test_update' => array(
+        t('Update the %field_name field.', array('%field_name' => t('A new base field'))),
+      ),
+    );
+    $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Original column deleted in shared table for new_base_field.');
+    $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__value'), 'Value column created in shared table for new_base_field.');
+    $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__format'), 'Format column created in shared table for new_base_field.');
+
+    // Remove the base field, ensure the update manager reports it, and the
+    // update deletes the schema.
+    $this->removeBaseField();
+    $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+    $expected = array(
+      'entity_test_update' => array(
+        t('Delete the %field_name field.', array('%field_name' => t('A new base field'))),
+      ),
+    );
+    $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_value'), 'Value column deleted from shared table for new_base_field.');
+    $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_format'), 'Format column deleted from shared table for new_base_field.');
+  }
+
+  /**
+   * Tests creating, updating, and deleting a bundle field if no entities exist.
+   */
+  public function testBundleFieldCreateUpdateDeleteWithoutData() {
+    // Add a bundle field, ensure the update manager reports it, and the update
+    // creates its schema.
+    $this->addBundleField();
+    $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+    $expected = array(
+      'entity_test_update' => array(
+        t('Create the %field_name field.', array('%field_name' => t('A new bundle field'))),
+      ),
+    );
+    $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertTrue($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.');
+
+    // Update the type of the base field from 'string' to 'text', ensure the
+    // update manager reports it, and the update adjusts the schema
+    // accordingly.
+    $this->modifyBundleField();
+    $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+    $expected = array(
+      'entity_test_update' => array(
+        t('Update the %field_name field.', array('%field_name' => t('A new bundle field'))),
+      ),
+    );
+    $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertTrue($this->database->schema()->fieldExists('entity_test_update__new_bundle_field', 'new_bundle_field_format'), 'Format column created in dedicated table for new_base_field.');
+
+    // Remove the bundle field, ensure the update manager reports it, and the
+    // update deletes the schema.
+    $this->removeBundleField();
+    $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+    $expected = array(
+      'entity_test_update' => array(
+        t('Delete the %field_name field.', array('%field_name' => t('A new bundle field'))),
+      ),
+    );
+    $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertFalse($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.');
+  }
+
+  /**
+   * Tests creating and deleting a base field if entities exist.
+   *
+   * This tests deletion when there are existing entities, but not existing data
+   * for the field being deleted.
+   *
+   * @see testBaseFieldDeleteWithExistingData()
+   */
+  public function testBaseFieldCreateDeleteWithExistingEntities() {
+    // Save an entity.
+    $name = $this->randomString();
+    $entity = $this->entityManager->getStorage('entity_test_update')->create(array('name' => $name));
+    $entity->save();
+
+    // Add a base field and run the update. Ensure the base field's column is
+    // created and the prior saved entity data is still there.
+    $this->addBaseField();
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.');
+    $entity = $this->entityManager->getStorage('entity_test_update')->load($entity->id());
+    $this->assertIdentical($entity->name->value, $name, 'Entity data preserved during field creation.');
+
+    // Remove the base field and run the update. Ensure the base field's column
+    // is deleted and the prior saved entity data is still there.
+    $this->removeBaseField();
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Column deleted from shared table for new_base_field.');
+    $entity = $this->entityManager->getStorage('entity_test_update')->load($entity->id());
+    $this->assertIdentical($entity->name->value, $name, 'Entity data preserved during field deletion.');
+  }
+
+  /**
+   * Tests creating and deleting a bundle field if entities exist.
+   *
+   * This tests deletion when there are existing entities, but not existing data
+   * for the field being deleted.
+   *
+   * @see testBundleFieldDeleteWithExistingData()
+   */
+  public function testBundleFieldCreateDeleteWithExistingEntities() {
     // Save an entity.
-    $this->entityManager->getStorage('entity_test_rev')->create()->save();
+    $name = $this->randomString();
+    $entity = $this->entityManager->getStorage('entity_test_update')->create(array('name' => $name));
+    $entity->save();
+
+    // Add a bundle field and run the update. Ensure the bundle field's table
+    // is created and the prior saved entity data is still there.
+    $this->addBundleField();
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertTrue($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.');
+    $entity = $this->entityManager->getStorage('entity_test_update')->load($entity->id());
+    $this->assertIdentical($entity->name->value, $name, 'Entity data preserved during field creation.');
+
+    // Remove the base field and run the update. Ensure the bundle field's
+    // table is deleted and the prior saved entity data is still there.
+    $this->removeBundleField();
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertFalse($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.');
+    $entity = $this->entityManager->getStorage('entity_test_update')->load($entity->id());
+    $this->assertIdentical($entity->name->value, $name, 'Entity data preserved during field deletion.');
+  }
+
+  /**
+   * Tests deleting a base field when it has existing data.
+   */
+  public function testBaseFieldDeleteWithExistingData() {
+    // Add the base field and run the update.
+    $this->addBaseField();
+    $this->entityDefinitionUpdateManager->applyUpdates();
+
+    // Save an entity with the base field populated.
+    $this->entityManager->getStorage('entity_test_update')->create(array('new_base_field' => 'foo'))->save();
 
-    // Restore entity_test_rev back to supporting revisions and try to apply
-    // the update. It's expected to throw an exception.
-    $this->state->delete('entity_test.entity_test_rev.disable_revisable');
+    // Remove the base field and apply updates. It's expected to throw an
+    // exception.
+    // @todo Revisit that expectation once purging is implemented for
+    //   all fields: https://www.drupal.org/node/2282119.
+    $this->removeBaseField();
+    try {
+      $this->entityDefinitionUpdateManager->applyUpdates();
+      $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to apply an update that deletes a non-purgeable field with data.');
+    }
+    catch (FieldStorageDefinitionUpdateForbiddenException $e) {
+      $this->pass('FieldStorageDefinitionUpdateForbiddenException thrown when trying to apply an update that deletes a non-purgeable field with data.');
+    }
+  }
+
+  /**
+   * Tests deleting a bundle field when it has existing data.
+   */
+  public function testBundleFieldDeleteWithExistingData() {
+    // Add the bundle field and run the update.
+    $this->addBundleField();
+    $this->entityDefinitionUpdateManager->applyUpdates();
+
+    // Save an entity with the bundle field populated.
+    entity_test_create_bundle('custom');
+    $this->entityManager->getStorage('entity_test_update')->create(array('type' => 'test_bundle', 'new_bundle_field' => 'foo'))->save();
+
+    // Remove the bundle field and apply updates. It's expected to throw an
+    // exception.
+    // @todo Revisit that expectation once purging is implemented for
+    //   all fields: https://www.drupal.org/node/2282119.
+    $this->removeBundleField();
+    try {
+      $this->entityDefinitionUpdateManager->applyUpdates();
+      $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to apply an update that deletes a non-purgeable field with data.');
+    }
+    catch (FieldStorageDefinitionUpdateForbiddenException $e) {
+      $this->pass('FieldStorageDefinitionUpdateForbiddenException thrown when trying to apply an update that deletes a non-purgeable field with data.');
+    }
+  }
+
+  /**
+   * Tests updating a base field when it has existing data.
+   */
+  public function testBaseFieldUpdateWithExistingData() {
+    // Add the base field and run the update.
+    $this->addBaseField();
+    $this->entityDefinitionUpdateManager->applyUpdates();
+
+    // Save an entity with the base field populated.
+    $this->entityManager->getStorage('entity_test_update')->create(array('new_base_field' => 'foo'))->save();
+
+    // Change the field's field type and apply updates. It's expected to
+    // throw an exception.
+    $this->modifyBaseField();
+    try {
+      $this->entityDefinitionUpdateManager->applyUpdates();
+      $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
+    }
+    catch (FieldStorageDefinitionUpdateForbiddenException $e) {
+      $this->pass('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
+    }
+  }
+
+  /**
+   * Tests updating a bundle field when it has existing data.
+   */
+  public function testBundleFieldUpdateWithExistingData() {
+    // Add the bundle field and run the update.
+    $this->addBundleField();
+    $this->entityDefinitionUpdateManager->applyUpdates();
+
+    // Save an entity with the bundle field populated.
+    entity_test_create_bundle('custom');
+    $this->entityManager->getStorage('entity_test_update')->create(array('type' => 'test_bundle', 'new_bundle_field' => 'foo'))->save();
+
+    // Change the field's field type and apply updates. It's expected to
+    // throw an exception.
+    $this->modifyBundleField();
+    try {
+      $this->entityDefinitionUpdateManager->applyUpdates();
+      $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
+    }
+    catch (FieldStorageDefinitionUpdateForbiddenException $e) {
+      $this->pass('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
+    }
+  }
+
+  /**
+   * Tests creating and deleting a multi-field index when there are no existing entities.
+   */
+  public function testEntityIndexCreateDeleteWithoutData() {
+    // Add an entity index and ensure the update manager reports that as an
+    // update to the entity type.
+    $this->addEntityIndex();
+    $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+    $expected = array(
+      'entity_test_update' => array(
+        t('Update the %entity_type entity type.', array('%entity_type' => $this->entityManager->getDefinition('entity_test_update')->getLabel())),
+      ),
+    );
+    $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
+
+    // Run the update and ensure the new index is created.
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
+
+    // Remove the index and ensure the update manager reports that as an
+    // update to the entity type.
+    $this->removeEntityIndex();
+    $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+    $expected = array(
+      'entity_test_update' => array(
+        t('Update the %entity_type entity type.', array('%entity_type' => $this->entityManager->getDefinition('entity_test_update')->getLabel())),
+      ),
+    );
+    $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
+
+    // Run the update and ensure the index is deleted.
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.');
+  }
+
+  /**
+   * Tests creating a multi-field index when there are existing entities.
+   */
+  public function testEntityIndexCreateWithData() {
+    // Save an entity.
+    $name = $this->randomString();
+    $entity = $this->entityManager->getStorage('entity_test_update')->create(array('name' => $name));
+    $entity->save();
+
+    // Add an entity index, run the update. For now, it's expected to throw an
+    // exception.
+    // @todo Improve SqlContentEntityStorageSchema::requiresEntityDataMigration()
+    //   to return FALSE when only index changes are required, so that it can be
+    //   applied on top of existing data: https://www.drupal.org/node/2340993.
+    $this->addEntityIndex();
     try {
       $this->entityDefinitionUpdateManager->applyUpdates();
       $this->fail('EntityStorageException thrown when trying to apply an update that requires data migration.');
@@ -113,4 +442,112 @@ public function testUpdateWithData() {
     }
   }
 
+  /**
+   * Updates the 'entity_test_update' entity type to revisionable.
+   */
+  protected function updateEntityTypeToRevisionable() {
+    $entity_type = clone $this->entityManager->getDefinition('entity_test_update');
+    $keys = $entity_type->getKeys();
+    $keys['revision'] = 'revision_id';
+    $entity_type->set('entity_keys', $keys);
+    $this->state->set('entity_test_update.entity_type', $entity_type);
+    $this->entityManager->clearCachedDefinitions();
+  }
+
+  /**
+   * Adds a new base field to the 'entity_test_update' entity type.
+   *
+   * @param string $type
+   *   (optional) The field type for the new field. Defaults to 'string'.
+   */
+  protected function addBaseField($type = 'string') {
+    $definitions['new_base_field'] = BaseFieldDefinition::create($type)
+      ->setName('new_base_field')
+      ->setLabel(t('A new base field'));
+    $this->state->set('entity_test_update.additional_base_field_definitions', $definitions);
+    $this->entityManager->clearCachedDefinitions();
+  }
+
+  /**
+   * Modifies the new base field from 'string' to 'text'.
+   */
+  protected function modifyBaseField() {
+    $this->addBaseField('text');
+  }
+
+  /**
+   * Removes the new base field from the 'entity_test_update' entity type.
+   */
+  protected function removeBaseField() {
+    $this->state->delete('entity_test_update.additional_base_field_definitions');
+    $this->entityManager->clearCachedDefinitions();
+  }
+
+  /**
+   * Adds a single-field index to the base field.
+   */
+  protected function addBaseFieldIndex() {
+    $this->state->set('entity_test_update.additional_field_index.entity_test_update.new_base_field', TRUE);
+    $this->entityManager->clearCachedDefinitions();
+  }
+
+  /**
+   * Removes the index added in addBaseFieldIndex().
+   */
+  protected function removeBaseFieldIndex() {
+    $this->state->delete('entity_test_update.additional_field_index.entity_test_update.new_base_field');
+    $this->entityManager->clearCachedDefinitions();
+  }
+
+  /**
+   * Adds a new bundle field to the 'entity_test_update' entity type.
+   *
+   * @param string $type
+   *   (optional) The field type for the new field. Defaults to 'string'.
+   */
+  protected function addBundleField($type = 'string') {
+    $definitions['new_bundle_field'] = FieldStorageDefinition::create($type)
+      ->setName('new_bundle_field')
+      ->setLabel(t('A new bundle field'))
+      ->setTargetEntityTypeId('entity_test_update');
+    $this->state->set('entity_test_update.additional_field_storage_definitions', $definitions);
+    $this->state->set('entity_test_update.additional_bundle_field_definitions.test_bundle', $definitions);
+    $this->entityManager->clearCachedDefinitions();
+  }
+
+  /**
+   * Modifies the new bundle field from 'string' to 'text'.
+   */
+  protected function modifyBundleField() {
+    $this->addBundleField('text');
+  }
+
+  /**
+   * Removes the new bundle field from the 'entity_test_update' entity type.
+   */
+  protected function removeBundleField() {
+    $this->state->delete('entity_test_update.additional_field_storage_definitions');
+    $this->state->delete('entity_test_update.additional_bundle_field_definitions.test_bundle');
+    $this->entityManager->clearCachedDefinitions();
+  }
+
+  /**
+   * Adds an index to the 'entity_test_update' entity type's base table.
+   *
+   * @see \Drupal\entity_test\EntityTestStorageSchema::getEntitySchema().
+   */
+  protected function addEntityIndex() {
+    $indexes = array(
+      'entity_test_update__new_index' => array('name', 'user_id'),
+    );
+    $this->state->set('entity_test_update.additional_entity_indexes', $indexes);
+  }
+
+  /**
+   * Removes the index added in addEntityIndex().
+   */
+  protected function removeEntityIndex() {
+    $this->state->delete('entity_test_update.additional_entity_indexes');
+  }
+
 }
diff --git a/core/modules/system/src/Tests/Entity/EntitySchemaTest.php b/core/modules/system/src/Tests/Entity/EntitySchemaTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..bdf08e3abd1ded8dd69b7b973233693c0b1f8bc0
--- /dev/null
+++ b/core/modules/system/src/Tests/Entity/EntitySchemaTest.php
@@ -0,0 +1,143 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Entity\EntitySchemaTest.
+ */
+
+namespace Drupal\system\Tests\Entity;
+
+use Drupal\Component\Utility\String;
+
+/**
+ * Tests adding a custom bundle field.
+ */
+class EntitySchemaTest extends EntityUnitTestBase  {
+
+  /**
+   * 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 Schema',
+      'description' => 'Tests entity field schema API for base and bundle fields.',
+      'group' => 'Entity API',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->installSchema('user', array('users_data'));
+    $this->installSchema('system', array('router'));
+    $this->database = $this->container->get('database');
+  }
+
+  /**
+   * Tests the custom bundle field creation and deletion.
+   */
+  public function testCustomFieldCreateDelete() {
+    // Install the module which adds the field.
+    $this->installModule('entity_schema_test');
+    $this->entityManager->clearCachedDefinitions();
+    $storage_definitions = $this->entityManager->getFieldStorageDefinitions('entity_test');
+    $this->assertNotNull($storage_definitions['custom_base_field'], 'Base field definition found.');
+    $this->assertNotNull($storage_definitions['custom_bundle_field'], 'Bundle field definition found.');
+
+    // Make sure the field schema can be created.
+    $this->entityManager->onFieldStorageDefinitionCreate($storage_definitions['custom_base_field']);
+    $this->entityManager->onFieldStorageDefinitionCreate($storage_definitions['custom_bundle_field']);
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
+    $table_mapping = $this->entityManager->getStorage('entity_test')->getTableMapping();
+    $base_table = current($table_mapping->getTableNames());
+    $base_column = current($table_mapping->getColumnNames('custom_base_field'));
+    $this->assertTrue($this->database->schema()->fieldExists($base_table, $base_column), 'Table column created');
+    $table = $table_mapping->getDedicatedDataTableName($storage_definitions['custom_bundle_field']);
+    $this->assertTrue($this->database->schema()->tableExists($table), 'Table created');
+
+    // Make sure the field schema can be deleted.
+    $this->uninstallModule('entity_schema_test');
+    $this->entityManager->onFieldStorageDefinitionDelete($storage_definitions['custom_base_field']);
+    $this->entityManager->onFieldStorageDefinitionDelete($storage_definitions['custom_bundle_field']);
+    $this->assertFalse($this->database->schema()->fieldExists($base_table, $base_column), 'Table column dropped');
+    $this->assertFalse($this->database->schema()->tableExists($table), 'Table dropped');
+  }
+
+  /**
+   * Updates the entity type definition.
+   *
+   * @param bool $alter
+   *   Whether the original definition should be altered or not.
+   */
+  protected function updateEntityType($alter) {
+    $entity_test_id = 'entity_test';
+    $original = $this->entityManager->getDefinition($entity_test_id);
+    $this->entityManager->clearCachedDefinitions();
+    $this->state->set('entity_schema_update', $alter);
+    $entity_type = $this->entityManager->getDefinition($entity_test_id);
+    $this->entityManager->onEntityTypeUpdate($entity_type, $original);
+  }
+
+  /**
+   * Tests that entity schema responds to changes in the entity type definition.
+   */
+  public function testEntitySchemaUpdate() {
+    $this->installModule('entity_schema_test');
+    $storage_definitions = $this->entityManager->getFieldStorageDefinitions('entity_test');
+    $this->entityManager->onFieldStorageDefinitionCreate($storage_definitions['custom_base_field']);
+    $this->entityManager->onFieldStorageDefinitionCreate($storage_definitions['custom_bundle_field']);
+    $schema_handler = $this->database->schema();
+    $tables = array('entity_test', 'entity_test_revision', 'entity_test_field_data', 'entity_test_field_revision');
+    $dedicated_tables = array('entity_test__custom_bundle_field', 'entity_test_revision__custom_bundle_field');
+
+    // Initially only the base table and the dedicated field data table should
+    // exist.
+    foreach ($tables as $index => $table) {
+      $this->assertEqual($schema_handler->tableExists($table), !$index, String::format('Entity schema correct for the @table table.', array('@table' => $table)));
+    }
+    $this->assertTrue($schema_handler->tableExists($dedicated_tables[0]), String::format('Field schema correct for the @table table.', array('@table' => $table)));
+
+    // Update the entity type definition and check that the entity schema now
+    // supports translations and revisions.
+    $this->updateEntityType(TRUE);
+    foreach ($tables as $table) {
+      $this->assertTrue($schema_handler->tableExists($table), String::format('Entity schema correct for the @table table.', array('@table' => $table)));
+    }
+    foreach ($dedicated_tables as $table) {
+      $this->assertTrue($schema_handler->tableExists($table), String::format('Field schema correct for the @table table.', array('@table' => $table)));
+    }
+
+    // Revert changes and check that the entity schema now does not support
+    // neither translations nor revisions.
+    $this->updateEntityType(FALSE);
+    foreach ($tables as $index => $table) {
+      $this->assertEqual($schema_handler->tableExists($table), !$index, String::format('Entity schema correct for the @table table.', array('@table' => $table)));
+    }
+    $this->assertTrue($schema_handler->tableExists($dedicated_tables[0]), String::format('Field schema correct for the @table table.', array('@table' => $table)));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function refreshServices() {
+    parent::refreshServices();
+    $this->database = $this->container->get('database');
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Entity/EntityUnitTestBase.php b/core/modules/system/src/Tests/Entity/EntityUnitTestBase.php
index 243f48dfe146b128d97319422c6cf83cd93ff43d..ada31f200ceb56d0e3af52b7be161c4a744f5aa8 100644
--- a/core/modules/system/src/Tests/Entity/EntityUnitTestBase.php
+++ b/core/modules/system/src/Tests/Entity/EntityUnitTestBase.php
@@ -111,4 +111,35 @@ protected function getHooksInfo() {
     return $hooks;
   }
 
+  /**
+   * Installs a module and refreshes services.
+   *
+   * @param string $module
+   *   The module to install.
+   */
+  protected function installModule($module) {
+    $this->enableModules(array($module));
+    $this->refreshServices();
+  }
+
+  /**
+   * Uninstalls a module and refreshes services.
+   *
+   * @param string $module
+   *   The module to uninstall.
+   */
+  protected function uninstallModule($module) {
+    $this->disableModules(array($module));
+    $this->refreshServices();
+  }
+
+  /**
+   * Refresh services.
+   */
+  protected function refreshServices() {
+    $this->container = \Drupal::getContainer();
+    $this->entityManager = $this->container->get('entity.manager');
+    $this->state = $this->container->get('state');
+  }
+
 }
diff --git a/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php b/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php
index 661095859f25661e628dca9d04b217d57f334fbd..9c3a87081258e6da6b46ded5b0f841d685c30514 100644
--- a/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php
+++ b/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php
@@ -8,7 +8,6 @@
 namespace Drupal\system\Tests\Entity;
 
 use Drupal\Core\Database\Database;
-use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\field\Entity\FieldStorageConfig;
 
@@ -460,19 +459,9 @@ function testFieldSqlStorageForeignKeys() {
     // Reload the field schema after the update.
     $schema = $field_storage->getSchema();
 
-    // Retrieve the field definition and check that the foreign key is in place.
-    $field_storage = FieldStorageConfig::loadByName('entity_test', $field_name);
+    // Check that the foreign key is in place.
     $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 = SqlContentEntityStorage::_fieldSqlSchema($field_storage);
-    $schema = $schemas[$this->table_mapping->getDedicatedDataTableName($field_storage)];
-    $this->assertEqual(count($schema['foreign keys']), 1, 'There is 1 foreign key in the schema');
-    $foreign_key = reset($schema['foreign keys']);
-    $foreign_key_column = $this->table_mapping->getFieldColumnName($field_storage, $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');
   }
 
   /**
diff --git a/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php b/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php
index 2874aed16c5ee9e97478d804fa08f9d62a4ac6c7..c2a6a657bba8b0e1193eec683654eb9ab926859e 100644
--- a/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php
+++ b/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php
@@ -87,24 +87,19 @@ protected function assertFieldStorageLangcode(ContentEntityInterface $entity, $m
 
     foreach ($fields as $field_name) {
       $field_storage = FieldStorageConfig::loadByName($entity_type, $field_name);
-      $tables = array(
-        $table_mapping->getDedicatedDataTableName($field_storage),
-        $table_mapping->getDedicatedRevisionTableName($field_storage),
-      );
+      $table = $table_mapping->getDedicatedDataTableName($field_storage);
 
-      foreach ($tables as $table) {
-        $record = \Drupal::database()
-          ->select($table, 'f')
-          ->fields('f')
-          ->condition('f.entity_id', $id)
-          ->condition('f.revision_id', $id)
-          ->execute()
-          ->fetchObject();
+      $record = \Drupal::database()
+        ->select($table, 'f')
+        ->fields('f')
+        ->condition('f.entity_id', $id)
+        ->condition('f.revision_id', $id)
+        ->execute()
+        ->fetchObject();
 
-        if ($record->langcode != $langcode) {
-          $status = FALSE;
-          break;
-        }
+      if ($record->langcode != $langcode) {
+        $status = FALSE;
+        break;
       }
     }
 
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
deleted file mode 100644
index 6732090ff52692ea9f050c9c22d14e9920d4bbb9..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.info.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-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
deleted file mode 100644
index 6065425733c9106260b12f540dda774f41d14ddf..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.install
+++ /dev/null
@@ -1,41 +0,0 @@
-<?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
deleted file mode 100644
index 7a39717821c4dbaba67bd1eb78efa1345a6641c0..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.module
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module for the entity API providing a bundle field.
- */
-
-use Drupal\Core\Field\BaseFieldDefinition;
-
-/**
- * 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
-    // BaseFieldDefinition as this should not implement FieldDefinitionInterface.
-    // See https://drupal.org/node/2280639.
-    $definitions['custom_field'] = BaseFieldDefinition::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'] = BaseFieldDefinition::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 = BaseFieldDefinition::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/system/tests/modules/entity_schema_test/entity_schema_test.info.yml b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..646717ddc2d413395af839dbb49c801a99f3770a
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.info.yml
@@ -0,0 +1,8 @@
+name: 'Entity schema test module'
+type: module
+description: 'Provides entity and field definitions to test entity schema.'
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+  - entity_test
diff --git a/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module
new file mode 100644
index 0000000000000000000000000000000000000000..206ffa23c25214a46447fc57c8f315419b94d375
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @file
+ * Test module for the entity API providing a bundle field.
+ */
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\entity_test\FieldStorageDefinition;
+use Drupal\entity_test\Entity\EntityTestMulRev;
+
+/**
+ * Implements hook_entity_type_alter().
+ */
+function entity_schema_test_entity_type_alter(array &$entity_types) {
+  if (\Drupal::state()->get('entity_schema_update')) {
+    $entity_type = $entity_types['entity_test'];
+    $entity_type->set('translatable', TRUE);
+    $entity_type->set('data_table', 'entity_test_field_data');
+    $keys = $entity_type->getKeys();
+    $keys['revision'] = 'revision_id';
+    $entity_type->set('entity_keys', $keys);
+  }
+}
+
+/**
+ * Implements hook_entity_base_field_info().
+ */
+function entity_schema_test_entity_base_field_info(EntityTypeInterface $entity_type) {
+  if ($entity_type->id() == 'entity_test') {
+    $definitions['custom_base_field'] = BaseFieldDefinition::create('string')
+      ->setName('custom_base_field')
+      ->setLabel(t('A custom base field'));
+    if (\Drupal::state()->get('entity_schema_update')) {
+      $definitions += EntityTestMulRev::baseFieldDefinitions($entity_type);
+    }
+    return $definitions;
+  }
+}
+
+/**
+ * Implements hook_entity_field_storage_info().
+ */
+function entity_schema_test_entity_field_storage_info(EntityTypeInterface $entity_type) {
+  if ($entity_type->id() == 'entity_test') {
+    $definitions['custom_bundle_field'] = FieldStorageDefinition::create('string')
+      ->setName('custom_bundle_field')
+      ->setLabel(t('A custom bundle field'))
+      ->setTargetEntityTypeId($entity_type->id());
+    return $definitions;
+  }
+}
+
+/**
+ * Implements hook_entity_bundle_field_info().
+ */
+function entity_schema_test_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle) {
+  if ($entity_type->id() == 'entity_test' && $bundle == 'custom') {
+    $definitions['custom_bundle_field'] = \Drupal::entityManager()->getFieldStorageDefinitions($entity_type->id())['custom_bundle_field'];
+    return $definitions;
+  }
+}
+
+/**
+ * Implements hook_entity_bundle_delete().
+ */
+function entity_schema_test_entity_bundle_delete($entity_type_id, $bundle) {
+  if ($entity_type_id == 'entity_test' && $bundle == 'custom') {
+    $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id);
+    $field_definitions = entity_schema_test_entity_bundle_field_info($entity_type, $bundle);
+    $field_definitions['custom_bundle_field']
+      ->setTargetEntityTypeId($entity_type_id)
+      ->setBundle($bundle);
+    // Notify the entity storage that our field is gone.
+    \Drupal::entityManager()->getStorage($entity_type_id)
+      ->onFieldDefinitionDelete($field_definitions['custom_bundle_field']);
+  }
+}
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index 63e289ab54b90175bbc388db90ab9b03a4fb81d9..d5b08fa4219f286ff44befcfcda11c47070b7594 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -73,13 +73,8 @@ function entity_test_entity_type_alter(array &$entity_types) {
     }
   }
 
-  // Optionally allow testing an entity type definition being updated from
-  // revisable to not or vice versa.
-  if (\Drupal::state()->get('entity_test.entity_test_rev.disable_revisable')) {
-    $keys = $entity_types['entity_test_rev']->getKeys();
-    unset($keys['revision']);
-    $entity_types['entity_test_rev']->set('entity_keys', $keys);
-  }
+  // Allow entity_test_update tests to override the entity type definition.
+  $entity_types['entity_test_update'] = \Drupal::state()->get('entity_test_update.entity_type', $entity_types['entity_test_update']);
 }
 
 /**
@@ -96,6 +91,15 @@ function entity_test_entity_base_field_info_alter(&$fields, EntityTypeInterface
   }
 }
 
+/**
+ * Implements hook_entity_field_storage_info().
+ */
+function entity_test_entity_field_storage_info(EntityTypeInterface $entity_type) {
+  if ($entity_type->id() == 'entity_test_update') {
+    return \Drupal::state()->get('entity_test_update.additional_field_storage_definitions', array());
+  }
+}
+
 /**
  * Creates a new bundle for entity_test entities.
  *
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestUpdate.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestUpdate.php
new file mode 100644
index 0000000000000000000000000000000000000000..9e792c7ddaf9fa2e8b303fedaea9012132b2dc43
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestUpdate.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\entity_test\Entity\EntityTestUpdate.
+ */
+
+namespace Drupal\entity_test\Entity;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Defines the test entity class for testing definition updates.
+ *
+ * This entity type starts out non-revisionable by lacking a "revision_id" key,
+ * but during an update test, can be made revisionable by adding that key.
+ *
+ * @ContentEntityType(
+ *   id = "entity_test_update",
+ *   label = @Translation("Test entity update"),
+ *   handlers = {
+ *     "storage_schema" = "Drupal\entity_test\EntityTestStorageSchema"
+ *   },
+ *   base_table = "entity_test_update",
+ *   revision_table = "entity_test_update_revision",
+ *   fieldable = TRUE,
+ *   persistent_cache = FALSE,
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "uuid" = "uuid",
+ *     "bundle" = "type",
+ *     "label" = "name"
+ *   }
+ * )
+ */
+class EntityTestUpdate extends EntityTestRev {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+    $fields += \Drupal::state()->get('entity_test_update.additional_base_field_definitions', array());
+    return $fields;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
+    $fields = parent::bundleFieldDefinitions($entity_type, $bundle, $base_field_definitions);
+    $fields += \Drupal::state()->get('entity_test_update.additional_bundle_field_definitions.' . $bundle, array());
+    return $fields;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/src/EntityTestStorageSchema.php b/core/modules/system/tests/modules/entity_test/src/EntityTestStorageSchema.php
new file mode 100644
index 0000000000000000000000000000000000000000..09e2ab1970d3a8e3dfba0f59b9f423064962e88e
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/EntityTestStorageSchema.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\entity_test\EntityTestStorageSchema.
+ */
+
+namespace Drupal\entity_test;
+
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
+/**
+ * Defines the entity_test_update storage_schema handler.
+ */
+class EntityTestStorageSchema extends SqlContentEntityStorageSchema {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
+    $schema = parent::getEntitySchema($entity_type, $reset);
+    $schema['entity_test_update']['indexes'] += \Drupal::state()->get('entity_test_update.additional_entity_indexes', array());
+    return $schema;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
+    $schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
+
+    if (\Drupal::state()->get('entity_test_update.additional_field_index.' . $table_name . '.' . $storage_definition->getName())) {
+      $this->addSharedTableFieldIndex($storage_definition, $schema);
+    }
+
+    return $schema;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/src/FieldStorageDefinition.php b/core/modules/system/tests/modules/entity_test/src/FieldStorageDefinition.php
new file mode 100644
index 0000000000000000000000000000000000000000..6a2757c473783a3783e06c5ce177c818b2d4a309
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/FieldStorageDefinition.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\entity_test\FieldStorageDefinition.
+ */
+
+namespace Drupal\entity_test;
+
+use Drupal\Core\Field\BaseFieldDefinition;
+
+/**
+ * A custom field storage definition class.
+ *
+ * For convenience we extend from BaseFieldDefinition although this should not
+ * implement FieldDefinitionInterface.
+ *
+ * @todo Provide and make use of a proper FieldStorageDefinition class instead:
+ *   https://drupal.org/node/2280639.
+ */
+class FieldStorageDefinition extends BaseFieldDefinition {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isBaseField() {
+    return FALSE;
+  }
+
+}
diff --git a/core/modules/taxonomy/src/TermStorageSchema.php b/core/modules/taxonomy/src/TermStorageSchema.php
index 83e3457c7cfa1c33c728e15a4c60a58b78d36985..301bf4688627715038d20c9f75f47949a616f8bf 100644
--- a/core/modules/taxonomy/src/TermStorageSchema.php
+++ b/core/modules/taxonomy/src/TermStorageSchema.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * Defines the term schema handler.
@@ -21,20 +22,10 @@ class TermStorageSchema extends SqlContentEntityStorageSchema {
   protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
     $schema = parent::getEntitySchema($entity_type, $reset = FALSE);
 
-    if (isset($schema['taxonomy_term_field_data'])) {
-      // Marking the respective fields as NOT NULL makes the indexes more
-      // performant.
-      $schema['taxonomy_term_field_data']['fields']['weight']['not null'] = TRUE;
-      $schema['taxonomy_term_field_data']['fields']['name']['not null'] = TRUE;
-
-      unset($schema['taxonomy_term_field_data']['indexes']['taxonomy_term_field__vid__target_id']);
-      unset($schema['taxonomy_term_field_data']['indexes']['taxonomy_term_field__description__format']);
-      $schema['taxonomy_term_field_data']['indexes'] += array(
-        'taxonomy_term__tree' => array('vid', 'weight', 'name'),
-        'taxonomy_term__vid_name' => array('vid', 'name'),
-        'taxonomy_term__name' => array('name'),
-      );
-    }
+    $schema['taxonomy_term_field_data']['indexes'] += array(
+      'taxonomy_term__tree' => array('vid', 'weight', 'name'),
+      'taxonomy_term__vid_name' => array('vid', 'name'),
+    );
 
     $schema['taxonomy_term_hierarchy'] = array(
       'description' => 'Stores the hierarchical relationship between terms.',
@@ -116,4 +107,32 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
     return $schema;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
+    $schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
+    $field_name = $storage_definition->getName();
+
+    if ($table_name == 'taxonomy_term_field_data') {
+      // Remove unneeded indexes.
+      unset($schema['indexes']['taxonomy_term_field__vid__target_id']);
+      unset($schema['indexes']['taxonomy_term_field__description__format']);
+
+      switch ($field_name) {
+        case 'weight':
+          // Improves the performance of the taxonomy_term__tree index defined
+          // in getEntitySchema().
+          $schema['fields'][$field_name]['not null'] = TRUE;
+          break;
+
+        case 'name':
+          $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
+          break;
+      }
+    }
+
+    return $schema;
+  }
+
 }
diff --git a/core/modules/user/src/UserStorageSchema.php b/core/modules/user/src/UserStorageSchema.php
index 0229db28bef1b6b5f15af15484c1b3fdaf0230ae..48690f74ae204c5327e77fb2893f74a312be390f 100644
--- a/core/modules/user/src/UserStorageSchema.php
+++ b/core/modules/user/src/UserStorageSchema.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * Defines the user schema handler.
@@ -21,20 +22,6 @@ class UserStorageSchema extends SqlContentEntityStorageSchema {
   protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
     $schema = parent::getEntitySchema($entity_type, $reset);
 
-    // The "users" table does not use serial identifiers.
-    $schema['users']['fields']['uid']['type'] = 'int';
-
-    // Marking the respective fields as NOT NULL makes the indexes more
-    // performant.
-    $schema['users_field_data']['fields']['access']['not null'] = TRUE;
-    $schema['users_field_data']['fields']['created']['not null'] = TRUE;
-    $schema['users_field_data']['fields']['name']['not null'] = TRUE;
-
-    $schema['users_field_data']['indexes'] += array(
-      'user__access' => array('access'),
-      'user__created' => array('created'),
-      'user__mail' => array('mail'),
-    );
     $schema['users_field_data']['unique keys'] += array(
       'user__name' => array('name', 'langcode'),
     );
@@ -71,4 +58,43 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
     return $schema;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function processIdentifierSchema(&$schema, $key) {
+    // The "users" table does not use serial identifiers.
+    if ($key != $this->entityType->getKey('id')) {
+      parent::processIdentifierSchema($schema, $key);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
+    $schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
+    $field_name = $storage_definition->getName();
+
+    if ($table_name == 'users_field_data') {
+      switch ($field_name) {
+        case 'name':
+          // Improves the performance of the user__name index defined
+          // in getEntitySchema().
+          $schema['fields'][$field_name]['not null'] = TRUE;
+          break;
+
+        case 'mail':
+          $this->addSharedTableFieldIndex($storage_definition, $schema);
+          break;
+
+        case 'access':
+        case 'created':
+          $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
+          break;
+      }
+    }
+
+    return $schema;
+  }
+
 }
diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php
index 18802c74d16bde027b34a463edb6cbc0ddaf2f61..e647387e400af6df9ceadcebd07626e5720bfda9 100644
--- a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php
@@ -53,11 +53,11 @@ 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',
     ]);
@@ -188,17 +188,17 @@ public function testGetFieldNames() {
    * @covers ::getColumnNames()
    */
   public function testGetColumnNames() {
-    $definitions['test'] = $this->setUpDefinition([]);
+    $definitions['test'] = $this->setUpDefinition('test', []);
     $table_mapping = new DefaultTableMapping($definitions);
     $expected = [];
     $this->assertSame($expected, $table_mapping->getColumnNames('test'));
 
-    $definitions['test'] = $this->setUpDefinition(['value']);
+    $definitions['test'] = $this->setUpDefinition('test', ['value']);
     $table_mapping = new DefaultTableMapping($definitions);
     $expected = ['value' => 'test'];
     $this->assertSame($expected, $table_mapping->getColumnNames('test'));
 
-    $definitions['test'] = $this->setUpDefinition(['value', 'format']);
+    $definitions['test'] = $this->setUpDefinition('test', ['value', 'format']);
     $table_mapping = new DefaultTableMapping($definitions);
     $expected = ['value' => 'test__value', 'format' => 'test__format'];
     $this->assertSame($expected, $table_mapping->getColumnNames('test'));
@@ -237,13 +237,21 @@ 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('isBaseField')
+      ->will($this->returnValue(TRUE));
+    $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/Entity/Sql/SqlContentEntityStorageSchemaTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php
index 3109adc4be794b1b0a35ee89a56ff74c71a69fa9..788744f500dfbba4da80d1ee83b6174cb45ecf28 100644
--- a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php
@@ -8,8 +8,8 @@
 namespace Drupal\Tests\Core\Entity\Sql;
 
 use Drupal\Core\Entity\ContentEntityType;
-use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
 use Drupal\Core\Entity\Sql\DefaultTableMapping;
+use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -47,11 +47,11 @@ class SqlContentEntityStorageSchemaTest extends UnitTestCase {
   protected $storageDefinitions;
 
   /**
-   * The content entity schema handler used in this test.
+   * The storage schema handler used in this test.
    *
    * @var \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema.
    */
-  protected $schemaHandler;
+  protected $storageSchema;
 
   /**
    * {@inheritdoc}
@@ -372,7 +372,7 @@ public function testGetSchemaBase() {
       ),
     );
 
-    $this->setUpEntitySchemaHandler($expected);
+    $this->setUpStorageSchema($expected);
 
     $table_mapping = new DefaultTableMapping($this->storageDefinitions);
     $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
@@ -382,7 +382,7 @@ public function testGetSchemaBase() {
       ->method('getTableMapping')
       ->will($this->returnValue($table_mapping));
 
-    $this->schemaHandler->onEntityTypeCreate($this->entityType);
+    $this->storageSchema->onEntityTypeCreate($this->entityType);
   }
 
   /**
@@ -472,7 +472,7 @@ public function testGetSchemaRevisionable() {
       ),
     );
 
-    $this->setUpEntitySchemaHandler($expected);
+    $this->setUpStorageSchema($expected);
 
     $table_mapping = new DefaultTableMapping($this->storageDefinitions);
     $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
@@ -482,7 +482,7 @@ public function testGetSchemaRevisionable() {
       ->method('getTableMapping')
       ->will($this->returnValue($table_mapping));
 
-    $this->schemaHandler->onEntityTypeCreate($this->entityType);
+    $this->storageSchema->onEntityTypeCreate($this->entityType);
   }
 
   /**
@@ -562,7 +562,7 @@ public function testGetSchemaTranslatable() {
       ),
     );
 
-    $this->setUpEntitySchemaHandler($expected);
+    $this->setUpStorageSchema($expected);
 
     $table_mapping = new DefaultTableMapping($this->storageDefinitions);
     $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
@@ -572,7 +572,7 @@ public function testGetSchemaTranslatable() {
       ->method('getTableMapping')
       ->will($this->returnValue($table_mapping));
 
-    $this->schemaHandler->onEntityTypeCreate($this->entityType);
+    $this->storageSchema->onEntityTypeCreate($this->entityType);
   }
 
   /**
@@ -746,7 +746,7 @@ public function testGetSchemaRevisionableTranslatable() {
       ),
     );
 
-    $this->setUpEntitySchemaHandler($expected);
+    $this->setUpStorageSchema($expected);
 
     $table_mapping = new DefaultTableMapping($this->storageDefinitions);
     $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
@@ -758,11 +758,301 @@ public function testGetSchemaRevisionableTranslatable() {
       ->method('getTableMapping')
       ->will($this->returnValue($table_mapping));
 
-    $this->schemaHandler->onEntityTypeCreate($this->entityType);
+    $this->storageSchema->onEntityTypeCreate($this->entityType);
+  }
+
+  /**
+   * Tests the schema for a field dedicated table.
+   *
+   * @covers ::getDedicatedTableSchema()
+   * @covers ::createDedicatedTableSchema()
+   */
+  public function testDedicatedTableSchema() {
+    $entity_type_id = 'entity_test';
+    $this->entityType = new ContentEntityType(array(
+      'id' => 'entity_test',
+      'entity_keys' => array('id' => 'id'),
+    ));
+
+    // Setup a field having a dedicated schema.
+    $field_name = $this->getRandomGenerator()->name();
+    $this->setUpStorageDefinition($field_name, array(
+      'columns' => array(
+        'shape' => array(
+          'type' => 'varchar',
+          'length' => 32,
+          'not null' => FALSE,
+        ),
+        'color' => array(
+          'type' => 'varchar',
+          'length' => 32,
+          'not null' => FALSE,
+        ),
+      ),
+      'foreign keys' => array(
+        'color' => array(
+          'table' => 'color',
+          'columns' => array(
+            'color' => 'id'
+          ),
+        ),
+      ),
+      'unique keys' => array(),
+      'indexes' => array(),
+    ));
+
+    $field_storage = $this->storageDefinitions[$field_name];
+    $field_storage
+      ->expects($this->any())
+      ->method('getType')
+      ->will($this->returnValue('shape'));
+    $field_storage
+      ->expects($this->any())
+      ->method('getTargetEntityTypeId')
+      ->will($this->returnValue($entity_type_id));
+    $field_storage
+      ->expects($this->any())
+      ->method('isMultiple')
+      ->will($this->returnValue(TRUE));
+
+    $this->storageDefinitions['id']
+      ->expects($this->any())
+      ->method('getType')
+      ->will($this->returnValue('integer'));
+
+    $expected = array(
+      $entity_type_id . '__' . $field_name => array(
+        'description' => "Data storage for $entity_type_id field $field_name.",
+        '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' => array(
+            'type' => 'int',
+            'unsigned' => true,
+            'not null' => true,
+            'description' => 'The entity id this data is attached to',
+          ),
+          'revision_id' => array(
+            'type' => 'int',
+            'unsigned' => true,
+            'not null' => true,
+            'description' => 'The entity revision id this data is attached to, which for an unversioned entity type is the same as the entity id',
+          ),
+          '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',
+          ),
+          $field_name . '_shape' => array(
+            'type' => 'varchar',
+            'length' => 32,
+            'not null' => false,
+          ),
+          $field_name . '_color' => array(
+            'type' => 'varchar',
+            'length' => 32,
+            'not null' => false,
+          ),
+        ),
+        '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'),
+        ),
+        'foreign keys' => array(
+          $field_name . '_color' => array(
+            'table' => 'color',
+            'columns' => array(
+              $field_name . '_color' => 'id',
+            ),
+          ),
+        ),
+      ),
+    );
+
+    $this->setUpStorageSchema($expected);
+
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
+    $table_mapping->setFieldNames($entity_type_id, array_keys($this->storageDefinitions));
+    $table_mapping->setExtraColumns($entity_type_id, array('default_langcode'));
+
+    $this->storage->expects($this->any())
+      ->method('getTableMapping')
+      ->will($this->returnValue($table_mapping));
+
+    $this->storageSchema->onFieldStorageDefinitionCreate($field_storage);
   }
 
   /**
-   * Sets up the schema handler.
+   * Tests the schema for a field dedicated table for an entity with a string identifier.
+   *
+   * @covers ::getDedicatedTableSchema()
+   * @covers ::createDedicatedTableSchema()
+   */
+  public function testDedicatedTableSchemaForEntityWithStringIdentifier() {
+    $entity_type_id = 'entity_test';
+    $this->entityType = new ContentEntityType(array(
+      'id' => 'entity_test',
+      'entity_keys' => array('id' => 'id'),
+    ));
+
+    // Setup a field having a dedicated schema.
+    $field_name = $this->getRandomGenerator()->name();
+    $this->setUpStorageDefinition($field_name, array(
+      'columns' => array(
+        'shape' => array(
+          'type' => 'varchar',
+          'length' => 32,
+          'not null' => FALSE,
+        ),
+        'color' => array(
+          'type' => 'varchar',
+          'length' => 32,
+          'not null' => FALSE,
+        ),
+      ),
+      'foreign keys' => array(
+        'color' => array(
+          'table' => 'color',
+          'columns' => array(
+            'color' => 'id'
+          ),
+        ),
+      ),
+      'unique keys' => array(),
+      'indexes' => array(),
+    ));
+
+    $field_storage = $this->storageDefinitions[$field_name];
+    $field_storage
+      ->expects($this->any())
+      ->method('getType')
+      ->will($this->returnValue('shape'));
+    $field_storage
+      ->expects($this->any())
+      ->method('getTargetEntityTypeId')
+      ->will($this->returnValue($entity_type_id));
+    $field_storage
+      ->expects($this->any())
+      ->method('isMultiple')
+      ->will($this->returnValue(TRUE));
+
+    $this->storageDefinitions['id']
+      ->expects($this->any())
+      ->method('getType')
+      ->will($this->returnValue('string'));
+
+    $expected = array(
+      $entity_type_id . '__' . $field_name => array(
+        'description' => "Data storage for $entity_type_id field $field_name.",
+        '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' => array(
+            'type' => 'varchar',
+            'length' => 128,
+            'not null' => true,
+            'description' => 'The entity id this data is attached to',
+          ),
+          'revision_id' => array(
+            'type' => 'varchar',
+            'length' => 128,
+            'not null' => true,
+            'description' => 'The entity revision id this data is attached to, which for an unversioned entity type is the same as the entity id',
+          ),
+          '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',
+          ),
+          $field_name . '_shape' => array(
+            'type' => 'varchar',
+            'length' => 32,
+            'not null' => false,
+          ),
+          $field_name . '_color' => array(
+            'type' => 'varchar',
+            'length' => 32,
+            'not null' => false,
+          ),
+        ),
+        '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'),
+        ),
+        'foreign keys' => array(
+          $field_name . '_color' => array(
+            'table' => 'color',
+            'columns' => array(
+              $field_name . '_color' => 'id',
+            ),
+          ),
+        ),
+      ),
+    );
+
+    $this->setUpStorageSchema($expected);
+
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
+    $table_mapping->setFieldNames($entity_type_id, array_keys($this->storageDefinitions));
+    $table_mapping->setExtraColumns($entity_type_id, array('default_langcode'));
+
+    $this->storage->expects($this->any())
+      ->method('getTableMapping')
+      ->will($this->returnValue($table_mapping));
+
+    $this->storageSchema->onFieldStorageDefinitionCreate($field_storage);
+  }
+
+  /**
+   * Sets up the storage schema object to test.
    *
    * This uses the field definitions set in $this->storageDefinitions.
    *
@@ -770,7 +1060,7 @@ public function testGetSchemaRevisionableTranslatable() {
    *   (optional) An associative array describing the expected entity schema to
    *   be created. Defaults to expecting nothing.
    */
-  protected function setUpEntitySchemaHandler(array $expected = array()) {
+  protected function setUpStorageSchema(array $expected = array()) {
     $this->entityManager->expects($this->any())
       ->method('getDefinition')
       ->with($this->entityType->id())
@@ -812,7 +1102,15 @@ protected function setUpEntitySchemaHandler(array $expected = array()) {
       ->method('schema')
       ->will($this->returnValue($db_schema_handler));
 
-    $this->schemaHandler = new SqlContentEntityStorageSchema($this->entityManager, $this->entityType, $this->storage, $connection);
+    $key_value = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreInterface');
+    $this->storageSchema = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema')
+      ->setConstructorArgs(array($this->entityManager, $this->entityType, $this->storage, $connection))
+      ->setMethods(array('installedStorageSchema'))
+      ->getMock();
+    $this->storageSchema
+      ->expects($this->any())
+      ->method('installedStorageSchema')
+      ->will($this->returnValue($key_value));
   }
 
   /**
@@ -826,7 +1124,10 @@ protected function setUpEntitySchemaHandler(array $expected = array()) {
    */
   public function setUpStorageDefinition($field_name, array $schema) {
     $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('isBaseField')
+      ->will($this->returnValue(TRUE));
+    // getName() is called once for each table.
     $this->storageDefinitions[$field_name]->expects($this->any())
       ->method('getName')
       ->will($this->returnValue($field_name));
diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php
index 24b90d3a01f3e383b40b5f52cc24f1ab8ba9a60f..6a6cef5fe9ddcc8fccebfb9076cdff1182741e97 100644
--- a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php
@@ -280,10 +280,7 @@ public function testOnEntityTypeCreate() {
       ),
     );
 
-    $this->fieldDefinitions['id'] = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');
-    $this->fieldDefinitions['id']->expects($this->any())
-      ->method('getName')
-      ->will($this->returnValue('id'));
+    $this->fieldDefinitions = $this->mockFieldDefinitions(array('id'));
     $this->fieldDefinitions['id']->expects($this->once())
       ->method('getColumns')
       ->will($this->returnValue($columns));
@@ -338,14 +335,22 @@ public function testOnEntityTypeCreate() {
 
     $storage = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorage')
       ->setConstructorArgs(array($this->entityType, $this->connection, $this->entityManager, $this->cache))
-      ->setMethods(array('schemaHandler'))
+      ->setMethods(array('getStorageSchema'))
       ->getMock();
 
-    $schema_handler = new SqlContentEntityStorageSchema($this->entityManager, $this->entityType, $storage, $this->connection);
+    $key_value = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreInterface');
+    $schema_handler = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema')
+      ->setConstructorArgs(array($this->entityManager, $this->entityType, $storage, $this->connection))
+      ->setMethods(array('installedStorageSchema', 'createSharedTableSchema'))
+      ->getMock();
+    $schema_handler
+      ->expects($this->any())
+      ->method('installedStorageSchema')
+      ->will($this->returnValue($key_value));
 
     $storage
       ->expects($this->any())
-      ->method('schemaHandler')
+      ->method('getStorageSchema')
       ->will($this->returnValue($schema_handler));
 
     $storage->onEntityTypeCreate($this->entityType);
@@ -921,83 +926,6 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent
     }
   }
 
-  /**
-   * Tests field SQL schema generation for an entity with a string identifier.
-   *
-   * @covers ::_fieldSqlSchema()
-   */
-  public function testFieldSqlSchemaForEntityWithStringIdentifier() {
-    $field_type_manager = $this->getMock('Drupal\Core\Field\FieldTypePluginManagerInterface');
-
-    $this->container->set('plugin.manager.field.field_type', $field_type_manager);
-    $this->container->set('entity.manager', $this->entityManager);
-
-    $this->entityType->expects($this->any())
-      ->method('getKey')
-      ->will($this->returnValueMap(array(
-        array('id', 'id'),
-        array('revision', 'revision'),
-      )));
-    $this->entityType->expects($this->once())
-      ->method('hasKey')
-      ->with('revision')
-      ->will($this->returnValue(TRUE));
-
-    $field_type_manager->expects($this->exactly(2))
-      ->method('getDefaultSettings')
-      ->will($this->returnValue(array()));
-    $field_type_manager->expects($this->exactly(2))
-      ->method('getDefaultInstanceSettings')
-      ->will($this->returnValue(array()));
-
-    $this->fieldDefinitions['id'] = BaseFieldDefinition::create('string')
-      ->setName('id');
-    $this->fieldDefinitions['revision'] = BaseFieldDefinition::create('string')
-      ->setName('revision');
-
-    $this->entityManager->expects($this->any())
-      ->method('getDefinition')
-      ->with('test_entity')
-      ->will($this->returnValue($this->entityType));
-    $this->entityManager->expects($this->any())
-      ->method('getBaseFieldDefinitions')
-      ->will($this->returnValue($this->fieldDefinitions));
-
-    // Define a field definition for a test_field field.
-    $field_storage = $this->getMock('\Drupal\field\FieldStorageConfigInterface');
-    $field_storage->deleted = FALSE;
-
-    $field_storage->expects($this->any())
-      ->method('getName')
-      ->will($this->returnValue('test_field'));
-
-    $field_storage->expects($this->any())
-      ->method('getTargetEntityTypeId')
-      ->will($this->returnValue('test_entity'));
-
-    $field_schema = array(
-      'columns' => array(
-        'value' => array(
-          'type' => 'varchar',
-          'length' => 10,
-          'not null' => FALSE,
-        ),
-      ),
-      'unique keys' => array(),
-      'indexes' => array(),
-      'foreign keys' => array(),
-    );
-    $field_storage->expects($this->any())
-      ->method('getSchema')
-      ->will($this->returnValue($field_schema));
-
-    $schema = SqlContentEntityStorage::_fieldSqlSchema($field_storage);
-
-    // 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');
-  }
-
   /**
    * @covers ::create()
    */
@@ -1071,6 +999,9 @@ protected function mockFieldDefinitions(array $field_names, $methods = array())
     $definition = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');
 
     // Assign common method return values.
+    $methods += array(
+      'isBaseField' => TRUE,
+    );
     foreach ($methods as $method => $result) {
       $definition
         ->expects($this->any())