diff --git a/core/core.services.yml b/core/core.services.yml
index dec001f..b0d9480 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -267,6 +267,9 @@ services:
     parent: container.trait
     tags:
       - { name: plugin_manager_cache_clear }
+  entity.schema.manager:
+    class: Drupal\Core\Entity\Schema\ContentEntitySchemaManager
+    arguments: ['@entity.manager', '@state']
   entity.form_builder:
     class: Drupal\Core\Entity\EntityFormBuilder
     arguments: ['@entity.manager', '@form_builder']
diff --git a/core/includes/update.inc b/core/includes/update.inc
index 945ffe7..e633f8b 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -13,6 +13,7 @@
 use Drupal\Core\Config\FileStorage;
 use Drupal\Core\Config\ConfigException;
 use Drupal\Core\DrupalKernel;
+use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Page\DefaultHtmlPageRenderer;
 use Drupal\Core\Utility\Error;
 use Drupal\Component\Uuid\Uuid;
@@ -278,6 +279,33 @@ function update_do_one($module, $number, $dependency_map, &$context) {
 }
 
 /**
+ * Performs entity schema updates.
+ *
+ * @param $module
+ *   The module whose update will be run.
+ * @param $number
+ *   The update number to run.
+ * @param $context
+ *   The batch context array.
+ */
+function update_entity_schema($module, $number, &$context) {
+  try {
+    \Drupal::service('entity.schema.manager')->applyChanges();
+  }
+  catch (EntityStorageException $e) {
+    watchdog_exception('update', $e);
+    $variables = Error::decodeException($e);
+    unset($variables['backtrace']);
+    // The exception message is run through
+    // \Drupal\Component\Utility\String::checkPlain() by
+    // \Drupal\Core\Utility\Error::decodeException().
+    $ret['#abort'] = array('success' => FALSE, 'query' => t('%type: !message in %function (line %line of %file).', $variables));
+    $context['results'][$module][$number] = $ret;
+    $context['results']['#abort'][] = 'update_entity_schema';
+  }
+}
+
+/**
  * Starts the database update batch process.
  *
  * @param $start
@@ -312,6 +340,13 @@ function update_batch($start, $redirect = NULL, $url = NULL, $batch = array(), $
     }
   }
 
+  // First of all perform entity schema updates, if needed, so that subsequent
+  // updates work with a correct entity schema.
+  $operations = array();
+  if (\Drupal::service('entity.schema.manager')->getChangeList()) {
+    $operations[] = array('update_entity_schema', array('system', '0 - Update entity schema'));
+  }
+
   // Resolve any update dependencies to determine the actual updates that will
   // be run and the order they will be run in.
   $updates = update_resolve_dependencies($start);
@@ -325,7 +360,7 @@ function update_batch($start, $redirect = NULL, $url = NULL, $batch = array(), $
     $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : array();
   }
 
-  $operations = array();
+  // Determine updates to be performed.
   foreach ($updates as $update) {
     if ($update['allowed']) {
       // Set the installed version of each module so updates will start at the
@@ -556,15 +591,17 @@ function update_get_update_function_list($starting_updates) {
   // Go through each module and find all updates that we need (including the
   // first update that was requested and any updates that run after it).
   $update_functions = array();
-  foreach ($starting_updates as $module => $version) {
-    $update_functions[$module] = array();
-    $updates = drupal_get_schema_versions($module);
-    if ($updates !== FALSE) {
-      $max_version = max($updates);
-      if ($version <= $max_version) {
-        foreach ($updates as $update) {
-          if ($update >= $version) {
-            $update_functions[$module][$update] = $module . '_update_' . $update;
+  if ($starting_updates) {
+    foreach ($starting_updates as $module => $version) {
+      $update_functions[$module] = array();
+      $updates = drupal_get_schema_versions($module);
+      if ($updates !== FALSE) {
+        $max_version = max($updates);
+        if ($version <= $max_version) {
+          foreach ($updates as $update) {
+            if ($update >= $version) {
+              $update_functions[$module][$update] = $module . '_update_' . $update;
+            }
           }
         }
       }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
index b39d900..27e4c77 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
@@ -11,15 +11,14 @@
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Database\Database;
-use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\Core\Entity\Query\QueryInterface;
 use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaProviderInterface;
 use Drupal\Core\Entity\Sql\DefaultTableMapping;
 use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\field\Entity\FieldStorageConfig;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -32,13 +31,12 @@
  *
  * The class uses \Drupal\Core\Entity\Schema\ContentEntitySchemaHandler
  * internally in order to automatically generate the database schema based on
- * the defined base fields. Entity types can override
- * ContentEntityDatabaseStorage::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
  */
-class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface {
+class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, ContentEntitySchemaProviderInterface {
 
   /**
    * The mapping of field columns to SQL tables.
@@ -108,7 +106,7 @@ class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements S
   /**
    * The entity schema handler.
    *
-   * @var \Drupal\Core\Entity\Schema\EntitySchemaHandlerInterface
+   * @var \Drupal\Core\Entity\Schema\ContentEntitySchemaHandlerInterface
    */
   protected $schemaHandler;
 
@@ -156,15 +154,40 @@ 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();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasData() {
+    // We cannot use an entity query as it relies on the entity type definition,
+    // which is not available while updating the entity schema.
+    return (bool) $this->database->select($this->baseTable)
+      ->countQuery()
+      ->range(0, 1)
+      ->execute()
+      ->fetchField();
+  }
+
+  /**
+   * 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';
@@ -222,13 +245,6 @@ public function getRevisionDataTable() {
   }
 
   /**
-   * {@inheritdoc}
-   */
-  public function getSchema() {
-    return $this->schemaHandler()->getSchema();
-  }
-
-  /**
    * Gets the schema handler for this storage controller.
    *
    * @return \Drupal\Core\Entity\Schema\ContentEntitySchemaHandler
@@ -236,24 +252,44 @@ public function getSchema() {
    */
   protected function schemaHandler() {
     if (!isset($this->schemaHandler)) {
-      $this->schemaHandler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this);
+      $this->schemaHandler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this, $this->database);
     }
     return $this->schemaHandler;
   }
 
   /**
+   * Updates the wrapped entity type definition.
+   *
+   * @param ContentEntityTypeInterface $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 setEntityType(ContentEntityTypeInterface $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())));
+    }
+  }
+
+  /**
    * {@inheritdoc}
    */
-  public function getTableMapping() {
-    if (!isset($this->tableMapping)) {
+  public function getTableMapping(array $storage_definitions = NULL) {
+    $table_mapping = $this->tableMapping;
+
+    if (!isset($this->tableMapping) || $storage_definitions) {
+      $definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
+      $base_field_definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
+      $table_mapping = new DefaultTableMapping($definitions, $base_field_definitions);
 
-      $definitions = array_filter($this->getFieldStorageDefinitions(), function (FieldStorageDefinitionInterface $definition) {
-        // @todo Remove the check for FieldDefinitionInterface::isMultiple() when
-        //   multiple-value base fields are supported in
-        //   https://drupal.org/node/2248977.
-        return !$definition->hasCustomStorage() && !$definition->isMultiple();
+      $definitions = array_filter($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);
@@ -281,16 +317,16 @@ public function getTableMapping() {
       $translatable = $this->entityType->getDataTable() && $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
@@ -299,7 +335,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
@@ -315,13 +351,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
@@ -330,20 +366,43 @@ 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);
+        }
+      }
+
+      if (!$storage_definitions) {
+        $this->tableMapping = $table_mapping;
+      }
     }
 
-    return $this->tableMapping;
+    return $table_mapping;
   }
 
   /**
@@ -590,7 +649,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);
@@ -1161,11 +1220,12 @@ protected function loadFieldItems(array $entities) {
     // Collect impacted fields.
     $storage_definitions = array();
     $definitions = array();
+    $table_mapping = $this->getTableMapping();
     foreach ($bundles as $bundle => $v) {
       $definitions[$bundle] = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle);
       foreach ($definitions[$bundle] as $field_name => $field_definition) {
         $storage_definition = $field_definition->getFieldStorageDefinition();
-        if ($this->usesDedicatedTable($storage_definition)) {
+        if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
           $storage_definitions[$field_name] = $storage_definition;
         }
       }
@@ -1174,7 +1234,7 @@ protected function loadFieldItems(array $entities) {
     // Load field data.
     $langcodes = array_keys(language_list(LanguageInterface::STATE_ALL));
     foreach ($storage_definitions as $field_name => $storage_definition) {
-      $table = $load_current ? static::_fieldTableName($storage_definition) : static::_fieldRevisionTableName($storage_definition);
+      $table = $load_current ? $table_mapping->getDedicatedDataTableName($storage_definition) : $table_mapping->getDedicatedRevisionTableName($storage_definition);
 
       // Ensure that only values having valid languages are retrieved. Since we
       // are loading values for multiple entities, we cannot limit the query to
@@ -1203,7 +1263,7 @@ protected function loadFieldItems(array $entities) {
             // For each column declared by the field, populate the item from the
             // prefixed database column.
             foreach ($storage_definition->getColumns() as $column => $attributes) {
-              $column_name = static::_fieldColumnName($storage_definition, $column);
+              $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
               // Unserialize the value if specified in the column schema.
               $item[$column] = (!empty($attributes['serialize'])) ? unserialize($row->$column_name) : $row->$column_name;
             }
@@ -1232,6 +1292,7 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
     $entity_type = $entity->getEntityTypeId();
     $default_langcode = $entity->getUntranslated()->language()->id;
     $translation_langcodes = array_keys($entity->getTranslationLanguages());
+    $table_mapping = $this->getTableMapping();
 
     if (!isset($vid)) {
       $vid = $id;
@@ -1239,11 +1300,11 @@ 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 = static::_fieldTableName($storage_definition);
-      $revision_name = static::_fieldRevisionTableName($storage_definition);
+      $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
+      $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
 
       // Delete and insert, rather than update, in case a value was added.
       if ($update) {
@@ -1254,20 +1315,24 @@ 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.
       $do_insert = FALSE;
       $columns = array('entity_id', 'revision_id', 'bundle', 'delta', 'langcode');
       foreach ($storage_definition->getColumns() as $column => $attributes) {
-        $columns[] = static::_fieldColumnName($storage_definition, $column);
+        $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) {
@@ -1285,12 +1350,14 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
             'langcode' => $langcode,
           );
           foreach ($storage_definition->getColumns() as $column => $attributes) {
-            $column_name = static::_fieldColumnName($storage_definition, $column);
+            $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
             // Serialize the value if specified in the column schema.
             $record[$column_name] = !empty($attributes['serialize']) ? serialize($item->$column) : $item->$column;
           }
           $query->values($record);
-          $revision_query->values($record);
+          if ($this->entityType->isRevisionable()) {
+            $revision_query->values($record);
+          }
 
           if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) {
             break;
@@ -1305,7 +1372,9 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
         if ($entity->isDefaultRevision()) {
           $query->execute();
         }
-        $revision_query->execute();
+        if ($this->entityType->isRevisionable()) {
+          $revision_query->execute();
+        }
       }
     }
   }
@@ -1317,19 +1386,22 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
    *   The entity.
    */
   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 = static::_fieldTableName($storage_definition);
-      $revision_name = static::_fieldRevisionTableName($storage_definition);
+      $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
+      $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
       $this->database->delete($table_name)
         ->condition('entity_id', $entity->id())
         ->execute();
-      $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();
+      }
     }
   }
 
@@ -1341,13 +1413,14 @@ protected function deleteFieldItems(EntityInterface $entity) {
    */
   protected function deleteFieldItemsRevision(EntityInterface $entity) {
     $vid = $entity->getRevisionId();
-    if (isset($vid)) {
+    if (isset($vid) && $this->entityType->isRevisionable()) {
+      $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 = static::_fieldRevisionTableName($storage_definition);
+        $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
         $this->database->delete($revision_name)
           ->condition('entity_id', $entity->id())
           ->condition('revision_id', $vid)
@@ -1357,151 +1430,127 @@ 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.
+   * {@inheritdoc}
    */
-  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();
+  public function requiresEntitySchemaChanges(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) {
+    return $this->schemaHandler()->requiresEntitySchemaChanges($definition, $original);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
-    $schema = $this->_fieldSqlSchema($storage_definition);
-    foreach ($schema as $name => $table) {
-      $this->database->schema()->createTable($name, $table);
-    }
+  public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) {
+    return $this->schemaHandler()->requiresFieldSchemaChanges($definition, $original);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
-    if (!$storage_definition->hasData()) {
-      // There is no data. Re-create the tables completely.
+  public function requiresEntityDataMigration(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) {
+    return $this->schemaHandler()->requiresEntityDataMigration($definition, $original);
+  }
 
-      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();
-      }
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldDataMigration(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) {
+    return $this->schemaHandler()->requiresFieldDataMigration($definition, $original);
+  }
 
-      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 = static::_fieldTableName($original);
-      $revision_table = static::_fieldRevisionTableName($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 = static::_fieldTableName($storage_definition);
-      $revision_table = static::_fieldRevisionTableName($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(
-                static::_fieldColumnName($storage_definition, $column_name[0]),
-                $column_name[1],
-              );
-            }
-            else {
-              $real_columns[] = static::_fieldColumnName($storage_definition, $column_name);
-            }
-          }
-          $this->database->schema()->addIndex($table, $real_name, $real_columns);
-          $this->database->schema()->addIndex($revision_table, $real_name, $real_columns);
-        }
-      }
-    }
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDefinitionCreate() {
+    $this->schemaHandler()->createEntitySchema($this->entityType);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDefinitionDelete() {
+    $this->schemaHandler()->dropEntitySchema($this->entityType);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDefinitionUpdate(EntityTypeInterface $original) {
+    // Ensure we have an updated entity type definition.
+    $this->entityType = $this->entityManager->getDefinition($this->entityTypeId);
+    // 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->schemaHandler()->updateEntitySchema($this->entityType, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
+    // 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->schemaHandler()->createFieldSchema($storage_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    $this->schemaHandler()->updateFieldSchema($storage_definition, $original);
   }
 
   /**
    * {@inheritdoc}
    */
   public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
-    // Mark all data associated with the field for deletion.
-    $table = static::_fieldTableName($storage_definition);
-    $revision_table = static::_fieldRevisionTableName($storage_definition);
-    $this->database->update($table)
-      ->fields(array('deleted' => 1))
-      ->execute();
+    $table_mapping = $this->getTableMapping();
+
+    if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+      // Mark all data associated with the field for deletion.
+      $table = $table_mapping->getDedicatedDataTableName($storage_definition);
+      $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
+      $this->database->update($table)
+        ->fields(array('deleted' => 1))
+        ->execute();
+      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 = static::_fieldTableName($storage_definition, TRUE);
-    $revision_new_table = static::_fieldRevisionTableName($storage_definition, TRUE);
-    $this->database->schema()->renameTable($table, $new_table);
-    $this->database->schema()->renameTable($revision_table, $revision_new_table);
+    // Update the field schema.
+    $this->schemaHandler()->prepareFieldSchemaDeletion($storage_definition);
   }
 
   /**
    * {@inheritdoc}
    */
   public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {
+    $table_mapping = $this->getTableMapping();
     $storage_definition = $field_definition->getFieldStorageDefinition();
-    $table_name = static::_fieldTableName($storage_definition);
-    $revision_name = static::_fieldRevisionTableName($storage_definition);
-
     // Mark field data as deleted.
-    $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();
+      }
+    }
   }
 
   /**
@@ -1516,21 +1565,24 @@ public function onBundleRename($bundle, $bundle_new) {
     // @todo Use the unified store of deleted field definitions instead in
     //   https://www.drupal.org/node/2282119
     $field_definitions += entity_load_multiple_by_properties('field_instance_config', array('entity_type' => $this->entityTypeId, 'bundle' => $bundle, 'deleted' => TRUE, 'include_deleted' => TRUE));
+    $table_mapping = $this->getTableMapping();
 
     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 = static::_fieldTableName($storage_definition, $is_deleted);
-        $revision_name = static::_fieldRevisionTableName($storage_definition, $is_deleted);
+        $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
+        $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
         $this->database->update($table_name)
           ->fields(array('bundle' => $bundle_new))
           ->condition('bundle', $bundle)
           ->execute();
-        $this->database->update($revision_name)
-          ->fields(array('bundle' => $bundle_new))
-          ->condition('bundle', $bundle)
-          ->execute();
+        if ($this->entityType->isRevisionable()) {
+          $this->database->update($revision_name)
+            ->fields(array('bundle' => $bundle_new))
+            ->condition('bundle', $bundle)
+            ->execute();
+        }
       }
     }
   }
@@ -1543,13 +1595,14 @@ protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definit
     // bundle fields.
     $storage_definition = $field_definition->getFieldStorageDefinition();
     $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
-    $table_name = static::_fieldTableName($storage_definition, $is_deleted);
+    $table_mapping = $this->getTableMapping();
+    $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
 
     // Get the entities which we want to purge first.
     $entity_query = $this->database->select($table_name, 't', array('fetch' => \PDO::FETCH_ASSOC));
     $or = $entity_query->orConditionGroup();
     foreach ($storage_definition->getColumns() as $column_name => $data) {
-      $or->isNotNull(static::_fieldColumnName($storage_definition, $column_name));
+      $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
     }
     $entity_query
       ->distinct(TRUE)
@@ -1560,7 +1613,7 @@ protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definit
     // Create a map of field data table column names to field column names.
     $column_map = array();
     foreach ($storage_definition->getColumns() as $column_name => $data) {
-      $column_map[static::_fieldColumnName($storage_definition, $column_name)] = $column_name;
+      $column_map[$table_mapping->getFieldColumnName($storage_definition, $column_name)] = $column_name;
     }
 
     $entities = array();
@@ -1600,374 +1653,93 @@ protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definit
   protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {
     $storage_definition = $field_definition->getFieldStorageDefinition();
     $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
-    $table_name = static::_fieldTableName($storage_definition, $is_deleted);
-    $revision_name = static::_fieldRevisionTableName($storage_definition, $is_deleted);
+    $table_mapping = $this->getTableMapping();
+    $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
+    $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
     $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id();
     $this->database->delete($table_name)
       ->condition('revision_id', $revision_id)
       ->execute();
-    $this->database->delete($revision_name)
-      ->condition('revision_id', $revision_id)
-      ->execute();
+    if ($this->entityType->isRevisionable()) {
+      $this->database->delete($revision_name)
+        ->condition('revision_id', $revision_id)
+        ->execute();
+    }
   }
 
   /**
    * {@inheritdoc}
    */
   public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
-    $table_name = static::_fieldTableName($storage_definition, TRUE);
-    $revision_name = static::_fieldRevisionTableName($storage_definition, TRUE);
-    $this->database->schema()->dropTable($table_name);
-    $this->database->schema()->dropTable($revision_name);
+    $this->schemaHandler()->deleteFieldSchema($storage_definition);
   }
 
   /**
    * {@inheritdoc}
    */
   public function countFieldData($storage_definition, $as_bool = FALSE) {
-    $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
-    $table_name = static::_fieldTableName($storage_definition, $is_deleted);
-
-    $query = $this->database->select($table_name, 't');
-    $or = $query->orConditionGroup();
-    foreach ($storage_definition->getColumns() as $column_name => $data) {
-      $or->isNotNull(static::_fieldColumnName($storage_definition, $column_name));
-    }
-    $query
-      ->condition($or)
-      ->fields('t', array('entity_id'))
-      ->distinct(TRUE);
-    // If we are performing the query just to check if the field has data
-    // limit the number of rows.
-    if ($as_bool) {
-      $query->range(0, 1);
-    }
-    $count = $query->countQuery()->execute()->fetchField();
-    return $as_bool ? (bool) $count : (int) $count;
-  }
-
-  /**
-   * Returns whether the passed field has been already deleted.
-   *
-   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
-   *   The field storage definition.
-   *
-   * @return bool
-   *   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) {
-    $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',
-      );
-    }
+    $table_mapping = $this->getTableMapping();
 
-    // 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',
-      );
+    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);
+      // 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();
     }
     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 = static::_fieldColumnName($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][] = static::_fieldColumnName($storage_definition, $column_name);
+      if ($as_bool) {
+        $count = $this->hasData();
       }
-    }
-
-    // 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(
-            static::_fieldColumnName($storage_definition, $column_name[0]),
-            $column_name[1],
-          );
+      else {
+        $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($table_mapping->getFieldColumnName($storage_definition, $column_name));
+          }
+          $query->condition($or);
         }
         else {
-          $current['indexes'][$real_name][] = static::_fieldColumnName($storage_definition, $column_name);
+          $query->isNotNull($storage_definition->getName());
         }
+        $count = $query
+          ->fields('t', array($this->idKey))
+          ->distinct(TRUE)
+          ->countQuery()
+          ->execute()
+          ->fetchField();
       }
     }
 
-    // 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 = static::_fieldColumnName($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(
-      static::_fieldTableName($storage_definition) => $current,
-      static::_fieldRevisionTableName($storage_definition) => $revision,
-    );
-  }
-
-  /**
-   * Generates a table name for a field data table.
-   *
-   * @private Calling this function circumvents the entity system and is
-   * strongly discouraged. This function is not considered part of the public
-   * API and modules relying on it might break even in minor releases. Only
-   * call this function to write a query that \Drupal::entityQuery() does not
-   * support. Always call entity_load() before using the data found in the
-   * table.
-   *
-   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
-   *   The field storage definition.
-   * @param bool $is_deleted
-   *   (optional) Whether the table name holding the values of a deleted field
-   *   should be returned.
-   *
-   * @return string
-   *   A string containing the generated name for the database table.
-   */
-  public static function _fieldTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) {
-    if ($is_deleted) {
-      // When a field is a deleted, the table is renamed to
-      // {field_deleted_data_FIELD_UUID}. To make sure we don't end up with
-      // table names longer than 64 characters, we hash the unique storage
-      // identifier and return the first 10 characters so we end up with a short
-      // unique ID.
-      return "field_deleted_data_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
-    }
-    else {
-      return static::_generateFieldTableName($storage_definition, FALSE);
-    }
-  }
-
-  /**
-   * Generates a table name for a field revision archive table.
-   *
-   * @private Calling this function circumvents the entity system and is
-   * strongly discouraged. This function is not considered part of the public
-   * API and modules relying on it might break even in minor releases. Only
-   * call this function to write a query that \Drupal::entityQuery() does not
-   * support. Always call entity_load() before using the data found in the
-   * table.
-   *
-   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
-   *   The field storage definition.
-   * @param bool $is_deleted
-   *   (optional) Whether the table name holding the values of a deleted field
-   *   should be returned.
-   *
-   * @return string
-   *   A string containing the generated name for the database table.
-   */
-  public static function _fieldRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) {
-    if ($is_deleted) {
-      // When a field is a deleted, the table is renamed to
-      // {field_deleted_revision_FIELD_UUID}. To make sure we don't end up with
-      // table names longer than 64 characters, we hash the unique storage
-      // identifier and return the first 10 characters so we end up with a short
-      // unique ID.
-      return "field_deleted_revision_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
-    }
-    else {
-      return static::_generateFieldTableName($storage_definition, TRUE);
-    }
-  }
-
-  /**
-   * Generates a safe and unanbiguous field table name.
-   *
-   * The method accounts for a maximum table name length of 64 characters, and
-   * takes care of disambiguation.
-   *
-   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
-   *   The field storage definition.
-   * @param bool $revision
-   *   TRUE for revision table, FALSE otherwise.
-   *
-   * @return string
-   *   The final table name.
-   */
-  protected static function _generateFieldTableName(FieldStorageDefinitionInterface $storage_definition, $revision) {
-    $separator = $revision ? '_revision__' : '__';
-    $table_name = $storage_definition->getTargetEntityTypeId() . $separator .  $storage_definition->getName();
-    // Limit the string to 48 characters, keeping a 16 characters margin for db
-    // prefixes.
-    if (strlen($table_name) > 48) {
-      // Use a shorter separator, a truncated entity_type, and a hash of the
-      // field UUID.
-      $separator = $revision ? '_r__' : '__';
-      // Truncate to the same length for the current and revision tables.
-      $entity_type = substr($storage_definition->getTargetEntityTypeId(), 0, 34);
-      $field_hash = substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
-      $table_name = $entity_type . $separator . $field_hash;
-    }
-    return $table_name;
-  }
-
-  /**
-   * 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 $as_bool ? (bool) $count : (int) $count;
   }
 
   /**
-   * Generates a column name for a field data table.
-   *
-   * @private Calling this function circumvents the entity system and is
-   * strongly discouraged. This function is not considered part of the public
-   * API and modules relying on it might break even in minor releases. Only
-   * call this function to write a query that \Drupal::entityQuery() does not
-   * support. Always call entity_load() before using the data found in the
-   * table.
+   * Returns whether the passed field has been already deleted.
    *
    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
    *   The field storage definition.
-   * @param string $column
-   *   The name of the column.
    *
-   * @return string
-   *   A string containing a generated column name for a field data table that is
-   *   unique among all other fields.
+   * @return bool
+   *   Whether the field has been already deleted.
    */
-  public static function _fieldColumnName(FieldStorageDefinitionInterface $storage_definition, $column) {
-    return in_array($column, FieldStorageConfig::getReservedColumns()) ? $column : $storage_definition->getName() . '_' . $column;
+  protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) {
+    return !array_key_exists($storage_definition->getName(), $this->entityManager->getFieldStorageDefinitions($this->entityTypeId));
   }
 
 }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
index 12aeca3..5460436 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
@@ -20,6 +20,20 @@ class ContentEntityNullStorage extends ContentEntityStorageBase {
   /**
    * {@inheritdoc}
    */
+  public function hasData() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function countFieldData($storage_definition, $as_bool = FALSE) {
+    return $as_bool ? FALSE : 0;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function loadMultiple(array $ids = NULL) {
     return array();
   }
@@ -131,11 +145,4 @@ protected function doSave($id, EntityInterface $entity) {
   protected function has($id, EntityInterface $entity) {
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function countFieldData($storage_definition, $as_bool = FALSE) {
-    return $as_bool ? FALSE : 0;
-  }
-
 }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
index a4849be..a3ca68b 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -78,6 +78,11 @@ protected function doCreate(array $values) {
   /**
    * {@inheritdoc}
    */
+  public function onEntityTypeDefinitionUpdate(EntityTypeInterface $original) { }
+
+  /**
+   * {@inheritdoc}
+   */
   public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { }
 
   /**
diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
index 3dc3bb6..b7b6f81 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
@@ -97,6 +97,16 @@ public function getEntityType() {
   /**
    * {@inheritdoc}
    */
+  public function hasData() {
+    return (bool) $this->getQuery()
+      ->range(0, 1)
+      ->count()
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function loadUnchanged($id) {
     $this->resetCache(array($id));
     return $this->load($id);
@@ -458,4 +468,22 @@ public function getQuery($conjunction = 'AND') {
     return \Drupal::entityQuery($this->getEntityTypeId(), $conjunction);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDefinitionCreate() {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDefinitionUpdate(EntityTypeInterface $original) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDefinitionDelete() {
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php
index bb086e6..65b531b 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php
@@ -33,6 +33,14 @@
   const FIELD_LOAD_REVISION = 'FIELD_LOAD_REVISION';
 
   /**
+   * Checks whether the storage contains at least one entity.
+   *
+   * @return bool
+   *   TRUE if the storage has data, FALSE otherwise.
+   */
+  public function hasData();
+
+  /**
    * Resets the internal, static entity cache.
    *
    * @param $ids
@@ -188,4 +196,22 @@ public function getEntityTypeId();
    */
   public function getEntityType();
 
+  /**
+   * Reacts to the creation of the entity type definition.
+   */
+  public function onEntityTypeDefinitionCreate();
+
+  /**
+   * Reacts to the update of the entity type definition.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $original
+   *   The original entity type definition.
+   */
+  public function onEntityTypeDefinitionUpdate(EntityTypeInterface $original);
+
+  /**
+   * Reacts to the deletion of the entity type definition.
+   */
+  public function onEntityTypeDefinitionDelete();
+
 }
diff --git a/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php b/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php
index 54e2bff..0149ec3 100644
--- a/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php
+++ b/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php
@@ -7,7 +7,10 @@
 
 namespace Drupal\Core\Entity\Exception;
 
+use Drupal\Core\Entity\EntityStorageException;
+
 /**
  * Exception thrown when a storage definition update is forbidden.
  */
-class FieldStorageDefinitionUpdateForbiddenException extends \Exception { }
+class FieldStorageDefinitionUpdateForbiddenException extends EntityStorageException {
+}
diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
index 23ce020..c2bc6d8 100644
--- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
@@ -10,10 +10,8 @@
 use Drupal\Core\Database\Query\SelectInterface;
 use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
-use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\Core\Entity\Query\QueryException;
 use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
-use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\field\FieldStorageConfigInterface;
 
 /**
@@ -116,11 +114,14 @@ public function addField($field, $type, $langcode) {
       if ($field_storage instanceof FieldStorageConfigInterface) {
         // Find the field column.
         $column = $field_storage->getMainPropertyName();
+        /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+        $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping();
+
         if ($key < $count) {
           $next = $specifiers[$key + 1];
           // Is this a field column?
           $columns = $field_storage->getColumns();
-          if (isset($columns[$next]) || in_array($next, FieldStorageConfig::getReservedColumns())) {
+          if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) {
             // Use it.
             $column = $next;
             // Do not process it again.
@@ -142,7 +143,7 @@ public function addField($field, $type, $langcode) {
           }
         }
         $table = $this->ensureFieldTable($index_prefix, $field_storage, $type, $langcode, $base_table, $entity_id_field, $field_id_field);
-        $sql_column = ContentEntityDatabaseStorage::_fieldColumnName($field_storage, $column);
+        $sql_column = $table_mapping->getFieldColumnName($field_storage, $column);
       }
       // This is an entity base field (non-configurable field).
       else {
@@ -220,11 +221,13 @@ protected function ensureEntityTable($index_prefix, $property, $type, $langcode,
   protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field) {
     $field_name = $field->getName();
     if (!isset($this->fieldTables[$index_prefix . $field_name])) {
-      $table = $this->sqlQuery->getMetaData('age') == EntityStorageInterface::FIELD_LOAD_CURRENT ? ContentEntityDatabaseStorage::_fieldTableName($field) : ContentEntityDatabaseStorage::_fieldRevisionTableName($field);
+      $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
+      /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+      $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping();
+      $table = $this->sqlQuery->getMetaData('age') == EntityStorageInterface::FIELD_LOAD_CURRENT ? $table_mapping->getDedicatedDataTableName($field) : $table_mapping->getDedicatedRevisionTableName($field);
       if ($field->getCardinality() != 1) {
         $this->sqlQuery->addMetaData('simple_query', FALSE);
       }
-      $entity_type = $this->sqlQuery->getMetaData('entity_type');
       $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode);
     }
     return $this->fieldTables[$index_prefix . $field_name];
diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
index 4f30d47..6863b9b 100644
--- a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
+++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
@@ -7,14 +7,27 @@
 
 namespace Drupal\Core\Entity\Schema;
 
+use Drupal\Component\Utility\String;
+use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
+use Drupal\Core\Field\FieldException;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * Defines a schema handler that supports revisionable, translatable entities.
  */
-class ContentEntitySchemaHandler implements EntitySchemaHandlerInterface {
+class ContentEntitySchemaHandler implements ContentEntitySchemaHandlerInterface, ContentEntitySchemaProviderInterface {
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
 
   /**
    * The entity type this schema builder is responsible for.
@@ -31,117 +44,1032 @@ class ContentEntitySchemaHandler implements EntitySchemaHandlerInterface {
   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.
    *
    * @var \Drupal\Core\Entity\ContentEntityDatabaseStorage
    */
-  protected $storage;
+  protected $storage;
+
+  /**
+   * A static cache of the generated schema array.
+   *
+   * @var array
+   */
+  protected $schema;
+
+  /**
+   * The database connection to be used.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The state service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * Constructs a ContentEntitySchemaHandler.
+   *
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type.
+   * @param \Drupal\Core\Entity\ContentEntityDatabaseStorage $storage
+   *   The storage of the entity type. This must be an SQL-based storage.
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database connection to be used.
+   */
+  public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorage $storage, Connection $database) {
+    $this->entityManager = $entity_manager;
+    $this->entityType = $entity_type;
+    $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id());
+    $this->storage = $storage;
+    $this->database = $database;
+  }
+
+  /**
+   * @return \Drupal\Core\State\StateInterface
+   */
+  protected function state() {
+    if (!isset($this->state)) {
+      $this->state = \Drupal::state();
+    }
+    return $this->state;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresEntitySchemaChanges(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) {
+    return !$original ||
+      $original->getStorageClass() != $definition->getStorageClass() ||
+      $original->isRevisionable() != $definition->isRevisionable() ||
+      $original->isTranslatable() != $definition->isTranslatable() ||
+      // Detect changes in key or index definitions.
+      $this->loadEntitySchemaData($original) != $this->getEntitySchemaData($definition, $this->getEntitySchema($definition, TRUE));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) {
+    return !$original ||
+      $original->getSchema() != $definition->getSchema() ||
+      $original->isRevisionable() != $definition->isRevisionable() ||
+      $original->hasCustomStorage() != $definition->hasCustomStorage() ||
+      $this->requiresFieldDataMigration($definition, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresEntityDataMigration(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) {
+    // A change in the storage class may or may not imply a data migration. We
+    // assume it does. This method should be overridden otherwise. Basically the
+    // only schema change that does not imply a data migration is from
+    // revisionable to non revisionable, as in that case we just need to drop
+    // revision tables.
+    return $original->getStorageClass() != $definition->getStorageClass() ||
+      $original->isRevisionable() != $definition->isRevisionable() ||
+      $original->isTranslatable() != $definition->isTranslatable();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldDataMigration(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) {
+    $table_mapping = $this->storage->getTableMapping();
+
+    // If the field changes its custom storage status, we will need to create or
+    // drop its schema. In any case we cannot migrate its data as custom storage
+    // is involved. Otherwise if a field is moved from a shared table to a
+    // dedicated table or viceversa we need a data migration.
+    $custom_storage = $original->hasCustomStorage() || $definition->hasCustomStorage();
+    $shared_table_changed = $table_mapping->allowsSharedTableStorage($original) != $table_mapping->allowsSharedTableStorage($definition);
+    $dedicated_table_changed = $table_mapping->requiresDedicatedTableStorage($original) != $table_mapping->requiresDedicatedTableStorage($definition);
+    if (!$custom_storage && ($shared_table_changed || $dedicated_table_changed)) {
+      return TRUE;
+    }
+    // If columns change we may need data manipulation, which we cannot handle.
+    if ($original->getColumns() != $definition->getColumns()) {
+      return TRUE;
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createEntitySchema(ContentEntityTypeInterface $entity_type) {
+    $this->checkEntityType($entity_type);
+    $schema_handler = $this->database->schema();
+    $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);
+      }
+    }
+    $this->saveEntitySchemaData($entity_type, $schema);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropEntitySchema(ContentEntityTypeInterface $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);
+
+    foreach ($this->getEntitySchemaTables() as $table_name) {
+      if ($schema_handler->tableExists($table_name)) {
+        $schema_handler->dropTable($table_name);
+      }
+    }
+
+    $this->storage->setEntityType($actual_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateEntitySchema(ContentEntityTypeInterface $entity_type, ContentEntityTypeInterface $original) {
+    $this->checkEntityType($entity_type);
+    $this->checkEntityType($original);
+
+    // If we have no data just recreate the entity schema from scratch.
+    if (!$this->database->schema()->tableExists($this->storage->getBaseTable()) || !$this->storage->hasData()) {
+      if ($this->database->supportsTransactionalDDL()) {
+        // If the database supports transactional DDL, we can go ahead and rely
+        // on it. If not, we will have to rollback manually if something fails.
+        $transaction = $this->database->startTransaction();
+      }
+      try {
+        $this->dropEntitySchema($original);
+        $this->createEntitySchema($entity_type);
+
+        // Update dedicated table revision schema.
+        if ($original->isRevisionable() && !$entity_type->isRevisionable()) {
+          $this->dropDedicatedTableRevisionSchema();
+        }
+        elseif (!$original->isRevisionable() && $entity_type->isRevisionable()) {
+          $this->createDedicatedTableRevisionSchema($entity_type);
+        }
+      }
+      catch (\Exception $e) {
+        if ($this->database->supportsTransactionalDDL()) {
+          $transaction->rollback();
+        }
+        else {
+          // Recreate original schema.
+          $this->createEntitySchema($original);
+        }
+        throw $e;
+      }
+    }
+    else {
+      $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);
+    }
+  }
+
+  /**
+   * Creates revision tables for the specified entity type.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type definition.
+   */
+  protected function createDedicatedTableRevisionSchema(ContentEntityTypeInterface $entity_type) {
+    $table_mapping = $this->storage->getTableMapping();
+    $schema_manager = $this->database->schema();
+    foreach ($this->fieldStorageDefinitions as $definition) {
+      if ($table_mapping->requiresDedicatedTableStorage($definition)) {
+        $schema = $this->getDedicatedTableSchema($definition, $entity_type);
+        $table_name = $table_mapping->getDedicatedRevisionTableName($definition);
+        $schema_manager->createTable($table_name, $schema[$table_name]);
+      }
+    }
+  }
+
+  /**
+   * Deletes revision tables for the specified entity type.
+   */
+  protected function dropDedicatedTableRevisionSchema() {
+    $table_mapping = $this->storage->getTableMapping();
+    $schema_manager = $this->database->schema();
+    foreach ($this->fieldStorageDefinitions as $definition) {
+      if ($table_mapping->requiresDedicatedTableStorage($definition)) {
+        $schema_manager->dropTable($table_mapping->getDedicatedRevisionTableName($definition));
+      }
+    }
+  }
+
+  /**
+   * Returns the entity schema for the specified entity type.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param bool $reset
+   *   (optional) If set to TRUE static cache will be ignored and a new schema
+   *   array generation will be performed. Defaults to FALSE.
+   *
+   * @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) {
+      // 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();
+
+      // Initialize the table schema.
+      $schema[$tables['base_table']] = $this->initializeBaseTable($entity_type);
+      if (isset($tables['revision_table'])) {
+        $schema[$tables['revision_table']] = $this->initializeRevisionTable($entity_type);
+      }
+      if (isset($tables['data_table'])) {
+        $schema[$tables['data_table']] = $this->initializeDataTable($entity_type);
+      }
+      if (isset($tables['revision_data_table'])) {
+        $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable($entity_type);
+      }
+
+      // We need to act only on shared entity schema tables.
+      $table_mapping = $this->storage->getTableMapping();
+      $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) {
+          if (!isset($storage_definitions[$field_name])) {
+            throw new FieldException(String::format('Fieled 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, $column_names));
+          }
+        }
+
+        // Add the schema for extra fields.
+        foreach ($table_mapping->getExtraColumns($table_name) as $column_name) {
+          if ($column_name == 'default_langcode') {
+            $this->addDefaultLangcodeSchema($schema[$table_name]);
+          }
+        }
+      }
+
+      // Process tables after having gathered field information.
+      $this->processBaseTable($entity_type, $schema[$tables['base_table']]);
+      if (isset($tables['revision_table'])) {
+        $this->processRevisionTable($entity_type, $schema[$tables['revision_table']]);
+      }
+      if (isset($tables['data_table'])) {
+        $this->processDataTable($entity_type, $schema[$tables['data_table']]);
+      }
+      if (isset($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];
+  }
+
+  /**
+   * Checks that we are dealing with the correct entity type.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type to be checked.
+   *
+   * @return bool
+   *   TRUE if the entity type matches the current one.
+   *
+   * @throws \Drupal\Core\Entity\EntityStorageException
+   */
+  protected function checkEntityType(ContentEntityTypeInterface $entity_type) {
+    if ($entity_type->id() != $this->entityType->id()) {
+      throw new EntityStorageException(String::format('Unsupported entity type @id', array('@id' => $entity_type->id())));
+    }
+    return TRUE;
+  }
+
+  /**
+   * Gets a list of entity type tables.
+   *
+   * @return array
+   *   A list of entity type tables, keyed by table key.
+   */
+  protected function getEntitySchemaTables() {
+    return array_filter(array(
+      'base_table' => $this->storage->getBaseTable(),
+      'revision_table' => $this->storage->getRevisionTable(),
+      'data_table' => $this->storage->getDataTable(),
+      'revision_data_table' => $this->storage->getRevisionDataTable(),
+    ));
+  }
+
+  /**
+   * Returns entity schema definitions for index and key definitions.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param array $schema
+   *   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 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'));
+
+    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);
+    }
+
+    return $schema_data;
+  }
+
+  /**
+   * Loads stored schema data for the given entity type definition.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type definition.
+   *
+   * @return array
+   *   The entity schema data array.
+   */
+  protected function loadEntitySchemaData(ContentEntityTypeInterface $entity_type) {
+    return $this->state()->get('entity.schema.handler.' . $entity_type->id() . '.schema_data') ?: array();
+  }
+
+  /**
+   * Stores schema data for the given entity type definition.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param array $schema
+   *   The entity schema data array.
+   */
+  protected function saveEntitySchemaData(ContentEntityTypeInterface $entity_type, $schema) {
+    $data = $this->getEntitySchemaData($entity_type, $schema);
+    $this->state()->set('entity.schema.handler.' . $entity_type->id() . '.schema_data', $data);
+  }
+
+  /**
+   * 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(ContentEntityTypeInterface $entity_type) {
+    $entity_type_id = $entity_type->id();
+
+    $schema = array(
+      'description' => "The base table for $entity_type_id entities.",
+      'primary key' => array($entity_type->getKey('id')),
+      'indexes' => array(),
+      'foreign keys' => array(),
+    );
+
+    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(),
+        'columns' => array($revision_key => $revision_key),
+      );
+    }
+
+    $this->addTableDefaults($schema);
+
+    return $schema;
+  }
+
+  /**
+   * 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(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(
+          'table' => $this->storage->getBaseTable(),
+          'columns' => array($id_key => $id_key),
+        ),
+      ),
+    );
+
+    $schema['indexes'][$this->getEntityIndexName($entity_type, $id_key)] = array($id_key);
+
+    $this->addTableDefaults($schema);
+
+    return $schema;
+  }
+
+  /**
+   * 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(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.",
+      // @todo Use the language entity key when https://drupal.org/node/2143729
+      //   is in.
+      'primary key' => array($id_key, 'langcode'),
+      'indexes' => array(),
+      'foreign keys' => array(
+        $entity_type_id => array(
+          'table' => $this->storage->getBaseTable(),
+          'columns' => array($id_key => $id_key),
+        ),
+      ),
+    );
+
+    if ($entity_type->hasKey('revision')) {
+      $key = $entity_type->getKey('revision');
+      $schema['indexes'][$this->getEntityIndexName($entity_type, $key)] = array($key);
+    }
+
+    $this->addTableDefaults($schema);
+
+    return $schema;
+  }
+
+  /**
+   * 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(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.",
+      // @todo Use the language entity key when https://drupal.org/node/2143729
+      //   is in.
+      'primary key' => array($revision_key, 'langcode'),
+      'indexes' => array(),
+      'foreign keys' => array(
+        $entity_type_id => array(
+          'table' => $this->storage->getBaseTable(),
+          'columns' => array($id_key => $id_key),
+        ),
+        $entity_type_id . '__revision' => array(
+          'table' => $this->storage->getRevisionTable(),
+          'columns' => array($revision_key => $revision_key),
+        )
+      ),
+    );
+
+    $this->addTableDefaults($schema);
+
+    return $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(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(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(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(ContentEntityTypeInterface $entity_type, array &$schema) {
+  }
+
+  /**
+   * 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);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createFieldSchema(FieldStorageDefinitionInterface $storage_definition) {
+    $this->performFieldSchemaOperation('create', $storage_definition);
+  }
+
+  /**
+   * Creates the schema for a field stored in a dedicated table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being created.
+   */
+  protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
+    $schema = $this->getDedicatedTableSchema($storage_definition);
+    foreach ($schema as $name => $table) {
+      $this->database->schema()->createTable($name, $table);
+    }
+  }
+
+  /**
+   * Creates the schema for a field stored in a shared table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being created.
+   */
+  protected function createSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
+    $created_field_name = $storage_definition->getName();
+    $table_mapping = $this->storage->getTableMapping();
+    $column_names = $table_mapping->getColumnNames($created_field_name);
+    $schema = $this->getSharedTableFieldSchema($storage_definition, $column_names);
+    $keys = array_diff_key($schema, array('fields' => FALSE));
+    $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.
+    foreach ($shared_table_names as $table_name) {
+      foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
+        if ($field_name == $created_field_name) {
+          foreach ($schema['fields'] as $column_name => $specifier) {
+            $this->database->schema()->addField($table_name, $column_name, $specifier, $keys);
+          }
+          // After creating the field schema skip to the next table.
+          break;
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepareFieldSchemaDeletion(FieldStorageDefinitionInterface $storage_definition) {
+    $table_mapping = $this->storage->getTableMapping();
+    // @todo Implement this also for shared table storage. See
+    //   https://www.drupal.org/node/2282119.
+    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);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteFieldSchema(FieldStorageDefinitionInterface $storage_definition) {
+    $this->performFieldSchemaOperation('delete', $storage_definition);
+  }
 
   /**
-   * A static cache of the generated schema array.
+   * Deletes the schema for a field stored in a dedicated table.
    *
-   * @var array
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being deleted.
    */
-  protected $schema;
+  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);
+    }
+  }
 
   /**
-   * Constructs a ContentEntitySchemaHandler.
+   * Deletes the schema for a field stored in a shared table.
    *
-   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
-   *   The entity manager.
-   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
-   *   The entity type.
-   * @param \Drupal\Core\Entity\ContentEntityDatabaseStorage $storage
-   *   The storage of the entity type. This must be an SQL-based storage.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being deleted.
    */
-  public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorage $storage) {
-    $this->entityType = $entity_type;
-    $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id());
-    $this->storage = $storage;
+  protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
+    $deleted_field_name = $storage_definition->getName();
+    $table_mapping = $this->storage->getTableMapping($this->originalDefinitions);
+    $column_names = $table_mapping->getColumnNames($deleted_field_name);
+    $schema = $this->getSharedTableFieldSchema($storage_definition, $column_names);
+    $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) {
+          // Drop indexes and unique keys first.
+          if (!empty($schema['indexes'])) {
+            foreach ($schema['indexes'] as $name => $specifier) {
+              $schema_handler->dropIndex($table_name, $name);
+            }
+          }
+          if (!empty($schema['unique keys'])) {
+            foreach ($schema['unique keys'] as $name => $specifier) {
+              $schema_handler->dropUniqueKey($table_name, $name);
+            }
+          }
+          // Drop columns.
+          foreach ($column_names as $column_name) {
+            $schema_handler->dropField($table_name, $column_name);
+          }
+          // After deleting the field schema skip to the next table.
+          break;
+        }
+      }
+    }
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getSchema() {
-    // Prepare basic information about the entity type.
-    $tables = $this->getTables();
+  public function updateFieldSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    // 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;
+  }
 
-    if (!isset($this->schema[$this->entityType->id()])) {
-      // Initialize the table schema.
-      $schema[$tables['base_table']] = $this->initializeBaseTable();
-      if (isset($tables['revision_table'])) {
-        $schema[$tables['revision_table']] = $this->initializeRevisionTable();
+  /**
+   * 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();
       }
-      if (isset($tables['data_table'])) {
-        $schema[$tables['data_table']] = $this->initializeDataTable();
+      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->deleteFieldSchema($original);
+        $this->createFieldSchema($storage_definition);
       }
-      if (isset($tables['revision_data_table'])) {
-        $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable();
+      catch (\Exception $e) {
+        if ($this->database->supportsTransactionalDDL()) {
+          $transaction->rollback();
+        }
+        else {
+          // Recreate tables.
+          $this->createFieldSchema($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();
-      foreach ($table_mapping->getTableNames() as $table_name) {
-        // Add the schema from field definitions.
-        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);
-        }
+      $table = $table_mapping->getDedicatedDataTableName($original);
+      $revision_table = $table_mapping->getDedicatedRevisionTableName($original);
 
-        // Add the schema for extra fields.
-        foreach ($table_mapping->getExtraColumns($table_name) as $column_name) {
-          if ($column_name == 'default_langcode') {
-            $this->addDefaultLangcodeSchema($schema[$table_name]);
+      $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);
         }
       }
+    }
+  }
 
-      // Process tables after having gathered field information.
-      $this->processBaseTable($schema[$tables['base_table']]);
-      if (isset($tables['revision_table'])) {
-        $this->processRevisionTable($schema[$tables['revision_table']]);
+  /**
+   * 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();
       }
-      if (isset($tables['data_table'])) {
-        $this->processDataTable($schema[$tables['data_table']]);
+      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->deleteFieldSchema($original);
+        $this->createFieldSchema($storage_definition);
       }
-      if (isset($tables['revision_data_table'])) {
-        $this->processRevisionDataTable($schema[$tables['revision_data_table']]);
+      catch (\Exception $e) {
+        if ($this->database->supportsTransactionalDDL()) {
+          $transaction->rollback();
+        }
+        else {
+          // Recreate original schema.
+          $this->createSharedTableSchema($original);
+        }
+        throw $e;
       }
-
-      $this->schema[$this->entityType->id()] = $schema;
     }
+    else {
+      if ($storage_definition->getColumns() != $original->getColumns()) {
+        throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data.");
+      }
 
-    return $this->schema[$this->entityType->id()];
-  }
+      $updated_field_name = $storage_definition->getName();
+      $table_mapping = $this->storage->getTableMapping();
+      $column_names = $table_mapping->getColumnNames($updated_field_name);
+      $original_schema = $this->getSharedTableFieldSchema($original, $column_names);
+      $schema = $this->getSharedTableFieldSchema($storage_definition, $column_names);
+      $schema_handler = $this->database->schema();
 
-  /**
-   * Gets a list of entity type tables.
-   *
-   * @return array
-   *   A list of entity type tables, keyed by table key.
-   */
-  protected function getTables() {
-    return array_filter(array(
-      'base_table' => $this->storage->getBaseTable(),
-      'revision_table' => $this->storage->getRevisionTable(),
-      'data_table' => $this->storage->getDataTable(),
-      'revision_data_table' => $this->storage->getRevisionDataTable(),
-    ));
+      // Iterate over the mapped table to find the ones that host the deleted
+      // field schema.
+      foreach ($table_mapping->getTableNames() as $table_name) {
+        foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
+          if ($field_name == $updated_field_name) {
+            // Drop original indexes and unique keys.
+            if (!empty($original_schema['indexes'])) {
+              foreach ($original_schema['indexes'] as $name => $specifier) {
+                $schema_handler->dropIndex($table_name, $name);
+              }
+            }
+            if (!empty($original_schema['unique keys'])) {
+              foreach ($original_schema['unique keys'] as $name => $specifier) {
+                $schema_handler->dropUniqueKey($table_name, $name);
+              }
+            }
+            // Create new indexes and unique keys.
+            if (!empty($schema['indexes'])) {
+              foreach ($schema['indexes'] as $name => $specifier) {
+                $schema_handler->addIndex($table_name, $name, $specifier);
+              }
+            }
+            if (!empty($schema['unique keys'])) {
+              foreach ($schema['unique keys'] as $name => $specifier) {
+                $schema_handler->addUniqueKey($table_name, $name, $specifier);
+              }
+            }
+            // After deleting the field schema skip to the next table.
+            break;
+          }
+        }
+      }
+    }
   }
 
   /**
    * Returns the schema for a single field definition.
    *
-   * @param array $schema
-   *   The table schema to add the field schema to, passed by reference.
-   * @param string $field_name
-   *   The name of the field.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field whose schema has to be returned.
    * @param string[] $column_mapping
    *   A mapping of field column names to database column names.
+   *
+   * @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 addFieldSchema(array &$schema, $field_name, array $column_mapping) {
-    $field_schema = $this->fieldStorageDefinitions[$field_name]->getSchema();
-    $field_description = $this->fieldStorageDefinitions[$field_name]->getDescription();
+  protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, array $column_mapping) {
+    $schema = array();
+    $field_schema = $storage_definition->getSchema();
+
+    // Check that the schema does not include forbidden column names.
+    if (array_intersect(array_keys($field_schema['columns']), $this->storage->getTableMapping()->getReservedColumns())) {
+      throw new FieldException(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];
@@ -166,19 +1094,18 @@ protected function addFieldSchema(array &$schema, $field_name, array $column_map
     }
 
     if (!empty($field_schema['indexes'])) {
-      $indexes = $this->getFieldIndexes($field_name, $field_schema, $column_mapping);
-      $schema['indexes'] = array_merge($schema['indexes'], $indexes);
+      $schema['indexes'] = $this->getFieldIndexes($field_name, $field_schema, $column_mapping);
     }
 
     if (!empty($field_schema['unique keys'])) {
-      $unique_keys = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping);
-      $schema['unique keys'] = array_merge($schema['unique keys'], $unique_keys);
+      $schema['unique keys'] = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping);
     }
 
     if (!empty($field_schema['foreign keys'])) {
-      $foreign_keys = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping);
-      $schema['foreign keys'] = array_merge($schema['foreign keys'], $foreign_keys);
+      $schema['foreign keys'] = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping);
     }
+
+    return $schema;
   }
 
   /**
@@ -337,133 +1264,170 @@ protected function addDefaultLangcodeSchema(&$schema) {
     );
   }
 
+
   /**
-   * Initializes common information for a base table.
+   * 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
-   *   A partial schema array for the base table.
+   *   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 initializeBaseTable() {
-    $entity_type_id = $this->entityType->id();
+  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()}.";
 
-    $schema = array(
-      'description' => "The base table for $entity_type_id entities.",
-      'primary key' => array($this->entityType->getKey('id')),
-      'indexes' => array(),
-      'foreign keys' => array(),
-    );
-
-    if ($this->entityType->hasKey('revision')) {
-      $revision_key = $this->entityType->getKey('revision');
-      $key_name = $this->getEntityIndexName($revision_key);
-      $schema['unique keys'][$key_name] = array($revision_key);
-      $schema['foreign keys'][$entity_type_id . '__revision'] = array(
-        'table' => $this->storage->getRevisionTable(),
-        'columns' => array($revision_key => $revision_key),
+    $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',
       );
     }
 
-    $this->addTableDefaults($schema);
-
-    return $schema;
-  }
-
-  /**
-   * Initializes common information for a revision table.
-   *
-   * @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');
+    // Define the revision ID schema, default to integer if there is no revision
+    // ID.
+    // @todo Revisit this code: the revision id should match the entity id type
+    //   if revisions are not supported.
+    $revision_id_definition = $this->entityType->isRevisionable() ? $this->fieldStorageDefinitions[$this->entityType->getKey('revision')] : NULL;
+    if (!$revision_id_definition || $revision_id_definition->getType() == 'integer') {
+      $revision_id_schema = array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => FALSE,
+        'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned',
+      );
+    }
+    else {
+      $revision_id_schema = array(
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => FALSE,
+        'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned',
+      );
+    }
 
-    $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(
-          'table' => $this->storage->getBaseTable(),
-          'columns' => array($id_key => $id_key),
+    $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',
         ),
-      ),
-    );
-
-    $schema['indexes'][$this->getEntityIndexName($id_key)] = array($id_key);
-
-    $this->addTableDefaults($schema);
-
-    return $schema;
-  }
-
-  /**
-   * Initializes common information for a data table.
-   *
-   * @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');
-
-    $schema = array(
-      'description' => "The data table for $entity_type_id entities.",
-      // @todo Use the language entity key when https://drupal.org/node/2143729
-      //   is in.
-      'primary key' => array($id_key, 'langcode'),
-      'indexes' => array(),
-      'foreign keys' => array(
-        $entity_type_id => array(
-          'table' => $this->storage->getBaseTable(),
-          'columns' => array($id_key => $id_key),
+        '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 ($this->entityType->hasKey('revision')) {
-      $key = $this->entityType->getKey('revision');
-      $schema['indexes'][$this->getEntityIndexName($key)] = array($key);
+    // 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())));
     }
 
-    $this->addTableDefaults($schema);
+    // 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;
+    }
 
-    return $schema;
-  }
+    // 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);
+        }
+      }
+    }
 
-  /**
-   * Initializes common information for a revision data table.
-   *
-   * @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');
+    // 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;
+      }
+    }
 
-    $schema = array(
-      'description' => "The revision data table for $entity_type_id entities.",
-      // @todo Use the language entity key when https://drupal.org/node/2143729
-      //   is in.
-      'primary key' => array($revision_key, 'langcode'),
-      'indexes' => array(),
-      'foreign keys' => array(
-        $entity_type_id => array(
-          'table' => $this->storage->getBaseTable(),
-          'columns' => array($id_key => $id_key),
-        ),
-        $entity_type_id . '__revision' => array(
-          'table' => $this->storage->getRevisionTable(),
-          'columns' => array($revision_key => $revision_key),
-        )
-      ),
-    );
+    $dedicated_table_schema = array($table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema);
 
-    $this->addTableDefaults($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 $schema;
+    return $dedicated_table_schema;
   }
 
   /**
@@ -482,53 +1446,34 @@ protected function addTableDefaults(&$schema) {
   }
 
   /**
-   * Processes the gathered schema for a base table.
-   *
-   * @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'));
-  }
-
-  /**
-   * Processes the gathered schema for a base table.
-   *
-   * @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'));
-  }
-
-  /**
-   * Processes the gathered schema for a base table.
+   * Returns the name to be used for the given entity index.
    *
-   * @param array $schema
-   *   The table schema, passed by reference.
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type.
+   * @param string $index
+   *   The index column name.
    *
-   * @return array
-   *   A partial schema array for the base table.
+   * @return string
+   *   The index name.
    */
-  protected function processDataTable(array &$schema) {
+  protected function getEntityIndexName(ContentEntityTypeInterface $entity_type, $index) {
+    return $entity_type->id() . '__' . $index;
   }
 
   /**
-   * Processes the gathered schema for a base table.
+   * Generates an index name for a field data table.
    *
-   * @param array $schema
-   *   The table schema, passed by reference.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   * @param string $index
+   *   The name of the index.
    *
-   * @return array
-   *   A partial schema array for the base table.
+   * @return string
+   *   A string containing a generated index name for a field data table that is
+   *   unique among all other fields.
    */
-  protected function processRevisionDataTable(array &$schema) {
+  protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
+    return $storage_definition->getName() . '_' . $index;
   }
 
   /**
@@ -546,17 +1491,4 @@ protected function processIdentifierSchema(&$schema, $key) {
     unset($schema['fields'][$key]['default']);
   }
 
-  /**
-   * Returns the name to be used for the given entity index.
-   *
-   * @param string $index
-   *   The index column name.
-   *
-   * @return string
-   *   The index name.
-   */
-  protected function getEntityIndexName($index) {
-    return $this->entityType->id() . '__' . $index;
-  }
-
 }
diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php
new file mode 100644
index 0000000..33a82b6
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\ContentEntitySchemaHandlerInterface.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
+/**
+ * Defines an interface for handling the storage schema of content entities.
+ */
+interface ContentEntitySchemaHandlerInterface {
+
+  /**
+   * Creates the schema for the given entity type definition.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type.
+   */
+  public function createEntitySchema(ContentEntityTypeInterface $entity_type);
+
+  /**
+   * Drops the schema for the given entity type definition.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The entity type.
+   */
+  public function dropEntitySchema(ContentEntityTypeInterface $entity_type);
+
+  /**
+   * Updates the schema when an entity type definition changes.
+   *
+   * When an entity type is changed to be revisionable or translatable, the
+   * entity schema needs to change accordingly.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+   *   The updated entity type definition.
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $original
+   *   The original entity type definition.
+   */
+  public function updateEntitySchema(ContentEntityTypeInterface $entity_type, ContentEntityTypeInterface $original);
+
+  /**
+   * Creates the storage schema for the given field.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being created.
+   */
+  public function createFieldSchema(FieldStorageDefinitionInterface $storage_definition);
+
+  /**
+   * Prepares the storage schema for the given field for deletion.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being deleted.
+   */
+  public function prepareFieldSchemaDeletion(FieldStorageDefinitionInterface $storage_definition);
+
+  /**
+   * Deletes the storage schema for the given field.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being deleted.
+   */
+  public function deleteFieldSchema(FieldStorageDefinitionInterface $storage_definition);
+
+  /**
+   * Updates the storage schema for the given field.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The storage definition of the field being updated.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original storage definition; i.e., the definition before the update.
+   *
+   * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
+   *   Thrown when the update to the field is forbidden.
+   */
+  public function updateFieldSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original);
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManager.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManager.php
new file mode 100644
index 0000000..12732d6
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManager.php
@@ -0,0 +1,386 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\ContentEntitySchemaManager.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+
+use Drupal\Component\Utility\String;
+use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\State\StateInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Manages changes in the entity and field schema.
+ */
+class ContentEntitySchemaManager implements ContentEntitySchemaManagerInterface {
+  use StringTranslationTrait;
+
+  /**
+   * The entity manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * The state service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   * @param StateInterface $state
+   */
+  public function __construct(EntityManagerInterface $entity_manager, StateInterface $state) {
+    $this->entityManager = $entity_manager;
+    $this->state = $state;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDefinitionCreate(ContentEntityTypeInterface $definition) {
+    $entity_type_id = $definition->id();
+    $storage = $this->entityManager->getStorage($entity_type_id);
+    // Notify the storage layer of the new entity type definition.
+    $storage->onEntityTypeDefinitionCreate();
+    // Store the current definitions to be able to track changes.
+    $this->saveEntityTypeDefinition($definition);
+    if ($definition->isFieldable()) {
+      $this->saveFieldStorageDefinitions($entity_type_id, $this->entityManager->getFieldStorageDefinitions($entity_type_id));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDefinitionUpdate(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) {
+    $entity_type_id = $definition->id();
+    $storage = $this->entityManager->getStorage($entity_type_id);
+    // Notify the storage layer of the new entity type definition.
+    $storage->onEntityTypeDefinitionUpdate($original);
+    // Store the current definitions to be able to track changes.
+    $this->saveEntityTypeDefinition($definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDefinitionDelete(ContentEntityTypeInterface $definition) {
+    $entity_type_id = $definition->id();
+    $storage = $this->entityManager->getStorage($entity_type_id);
+    // Notify the storage layer of the entity type definition deletion.
+    $storage->onEntityTypeDefinitionDelete();
+    // Store the current definitions to be able to track changes.
+    $this->deleteEntityTypeDefinition($entity_type_id);
+    // Ensure we delete any data concerning this entity type. It might have
+    // switched from fieldable to non-fieldable during its life cycle.
+    $this->deleteFieldStorageDefinitions($entity_type_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $definition) {
+    $entity_type_id = $definition->getTargetEntityTypeId();
+    /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */
+    $storage = $this->entityManager->getStorage($entity_type_id);
+    // Notify the storage layer of the new field storage definition.
+    $storage->onFieldStorageDefinitionCreate($definition);
+    // Update our field storage definitions.
+    $definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+    $definitions[$definition->getName()] = $definition;
+    $this->saveFieldStorageDefinitions($entity_type_id, $definitions);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) {
+    $entity_type_id = $definition->getTargetEntityTypeId();
+    /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */
+    $storage = $this->entityManager->getStorage($entity_type_id);
+    // Notify the storage layer of the new field storage definition.
+    $storage->onFieldStorageDefinitionUpdate($definition, $original);
+    // Update our field storage definitions.
+    $definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+    $definitions[$definition->getName()] = $definition;
+    $this->saveFieldStorageDefinitions($entity_type_id, $definitions);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $definition) {
+    $entity_type_id = $definition->getTargetEntityTypeId();
+    /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */
+    $storage = $this->entityManager->getStorage($entity_type_id);
+    // Notify the storage layer of the removed field storage definition.
+    $storage->onFieldStorageDefinitionDelete($definition);
+    // Update our field storage definitions.
+    $definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+    unset($definitions[$definition->getName()]);
+    $this->saveFieldStorageDefinitions($entity_type_id, $definitions);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChangeList($entity_type_id = NULL) {
+    $change_list = array();
+    $definitions = array_filter($this->entityManager->getDefinitions(), function($definition) { return $definition instanceof ContentEntityTypeInterface; });
+    $entity_type_ids = isset($entity_type_id) ? array($entity_type_id) : array_keys($definitions);
+
+    foreach ($entity_type_ids as $entity_type_id) {
+      $definition = $definitions[$entity_type_id];
+      $storage = $this->entityManager->getStorage($entity_type_id);
+
+      if ($definition instanceof ContentEntityTypeInterface && $storage instanceof ContentEntitySchemaProviderInterface) {
+        // Check whether there are changes in the entity type definition that
+        // would affect entity schema.
+        $original = $this->loadEntityTypeDefinition($entity_type_id);
+        if ($storage->requiresEntitySchemaChanges($definition, $original)) {
+          $change_list[$entity_type_id]['entity_type'] = static::DEFINITION_UPDATED;
+          if ($storage->requiresEntityDataMigration($definition, $original)) {
+            $change_list[$entity_type_id]['data_migration'] = TRUE;
+          }
+        }
+
+        // Check whether there are changes in the field storage definitions that
+        // would affect entity schema. We skip definitions with custom storage
+        // as they do not affect entity schema.
+        if ($definition->isFieldable()) {
+          $field_changes = array();
+          $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+          $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
+
+          // Detect created field storage definitions.
+          $created = array_filter(array_diff_key($storage_definitions, $original_storage_definitions), function(FieldStorageDefinitionInterface $definition) { return !$definition->hasCustomStorage(); });
+          $field_changes = array_merge($field_changes, array_map(function() { return static::DEFINITION_CREATED; }, $created));
+
+          // Detect deleted field storage definitions.
+          $deleted = array_filter(array_diff_key($original_storage_definitions, $storage_definitions), function(FieldStorageDefinitionInterface $definition) { return !$definition->hasCustomStorage(); });
+          $field_changes = array_merge($field_changes, array_map(function() { return static::DEFINITION_DELETED; }, $deleted));
+
+          // Now compare field storage definitions.
+          foreach (array_intersect_key($storage_definitions, $original_storage_definitions) as $field_name => $definition) {
+            /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $definition */
+            if (!$definition->hasCustomStorage()) {
+              $original = $this->loadFieldStorageDefinitions($definition->getTargetEntityTypeId())[$definition->getName()];
+              if ($storage->requiresFieldSchemaChanges($original, $definition)) {
+                $field_changes[$field_name] = static::DEFINITION_UPDATED;
+                if ($storage->requiresFieldDataMigration($original, $definition)) {
+                  $change_list[$entity_type_id]['data_migration'] = TRUE;
+                }
+              }
+            }
+          }
+
+          if ($field_changes) {
+            $change_list[$entity_type_id]['field_storage_definitions'] = $field_changes;
+          }
+        }
+      }
+    }
+
+    return array_filter($change_list);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyChanges($entity_type_id = NULL) {
+    foreach ($this->getChangeList($entity_type_id) as $entity_type_id => $change_list) {
+      $storage = $this->entityManager->getStorage($entity_type_id);
+
+      try {
+        $has_data = $storage->hasData();
+      }
+      catch (DatabaseExceptionWrapper $e) {
+        // The entity schema might be corrupted. In this case it is safer to
+        // assume there is data available, to avoid performing unrecoverable
+        // operations.
+        $has_data = TRUE;
+      }
+
+      // We do not allow any kind of schema change that would imply a data
+      // migration.
+      if (empty($change_list['data_migration']) || !$has_data) {
+        // Process entity type definition changes.
+        if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) {
+          /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $definition */
+          $definition = $this->entityManager->getDefinition($entity_type_id);
+          $this->onEntityTypeDefinitionUpdate($definition, $this->loadEntityTypeDefinition($entity_type_id));
+        }
+
+        // Process field storage definition changes.
+        if (!empty($change_list['field_storage_definitions'])) {
+          $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
+          $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+
+          foreach ($change_list['field_storage_definitions'] as $field_name => $change) {
+            switch ($change) {
+              case static::DEFINITION_CREATED:
+                $this->onFieldStorageDefinitionCreate($storage_definitions[$field_name]);
+                break;
+
+              case static::DEFINITION_UPDATED:
+                $this->onFieldStorageDefinitionUpdate($storage_definitions[$field_name], $original_storage_definitions[$field_name]);
+                break;
+
+              case static::DEFINITION_DELETED:
+                $this->onFieldStorageDefinitionDelete($storage_definitions[$field_name]);
+                break;
+            }
+          }
+        }
+      }
+      else {
+        $args = array('@entity_type_id' => $entity_type_id);
+        $message = String::format('Changes for the @entity_type_id entity type involve a data migration and cannot be applied.', $args);
+        throw new EntityStorageException($message);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChangeSummary($entity_type_id = NULL) {
+    $summary = array();
+
+    foreach ($this->getChangeList($entity_type_id) as $entity_type_id => $change_list) {
+      // Process entity type definition changes.
+      if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) {
+        $definition = $this->entityManager->getDefinition($entity_type_id);
+        $summary[$entity_type_id][] = $this->t('The %entity_type entity type has schema changes.', array('%entity_type' => $definition->getLabel()));
+      }
+
+      // Process field storage definition changes.
+      if (!empty($change_list['field_storage_definitions'])) {
+        $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
+        $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+
+        foreach ($change_list['field_storage_definitions'] as $field_name => $change) {
+          $definition = isset($storage_definitions[$field_name]) ? $storage_definitions[$field_name] : $original_storage_definitions[$field_name];
+          $args = array('%field_name' => $definition->getLabel());
+
+          switch ($change) {
+            case static::DEFINITION_CREATED:
+              $summary[$entity_type_id][] = $this->t('The %field_name field has been created.', $args);
+              break;
+
+            case static::DEFINITION_UPDATED:
+              $summary[$entity_type_id][] = $this->t('The %field_name field has schema changes.', $args);
+              break;
+
+            case static::DEFINITION_DELETED:
+              $summary[$entity_type_id][] = $this->t('The %field_name field has been deleted.', $args);
+              break;
+          }
+        }
+      }
+    }
+
+    return $summary;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSystemRequirements($phase) {
+    $requirements = array(
+      'title' => t('Entity schema'),
+    );
+
+    if ($this->getChangeList()) {
+      $requirements['value'] = $this->t('Out of date');
+      $requirements['severity'] = REQUIREMENT_ERROR;
+      $requirements['description'] = $requirements['update']['description'] = $this->t('Some entity types have schema updates to install. You should run the <a href="@update">database update script</a> immediately.', array('@update' => base_path() . 'core/update.php'));
+    }
+    else {
+      $requirements['value'] = $this->t('Up to date');
+    }
+
+    return $requirements;
+  }
+
+  /**
+   * Returns the specified stored entity type definition.
+   *
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   *
+   * @return \Drupal\Core\Entity\ContentEntityTypeInterface
+   *   A stored entity type definition.
+   */
+  protected function loadEntityTypeDefinition($entity_type_id) {
+    return $this->state->get('entity.schema.manager.' . $entity_type_id . '.entity_type');
+  }
+
+  /**
+   * Stores the specified stored entity type definition.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $definition
+   *   The entity type definition.
+   */
+  protected function saveEntityTypeDefinition(ContentEntityTypeInterface $definition) {
+    $entity_type_id = $definition->id();
+    $this->state->set('entity.schema.manager.' . $entity_type_id . '.entity_type', $definition);
+  }
+
+  /**
+   * Deletes the specified stored entity type.
+   *
+   * @param string $entity_type_id
+   *   The entity type definition identifier.
+   */
+  protected function deleteEntityTypeDefinition($entity_type_id) {
+    $this->state->delete('entity.schema.manager.' . $entity_type_id . '.entity_type');
+  }
+
+  /**
+   * Returns the stored field storage definitions for the specified entity type.
+   *
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   *
+   * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[]
+   *   An array of field storage definitions.
+   */
+  protected function loadFieldStorageDefinitions($entity_type_id) {
+    return $this->state->get('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions');
+  }
+
+  /**
+   * Stores the field storage definitions for the specified entity type.
+   *
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
+   *   An array of field storage definitions.
+   */
+  protected function saveFieldStorageDefinitions($entity_type_id, array $storage_definitions) {
+    $this->state->set('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions', $storage_definitions);
+  }
+
+  /**
+   * Deletes the stored field storage definitions for the specified entity type.
+   *
+   * @param string $entity_type_id
+   *   The entity type definition identifier.
+   */
+  protected function deleteFieldStorageDefinitions($entity_type_id) {
+    $this->state->delete('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions');
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManagerInterface.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManagerInterface.php
new file mode 100644
index 0000000..a8cf709
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManagerInterface.php
@@ -0,0 +1,149 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\ContentEntitySchemaManagerInterface.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
+/**
+ * Defines an interface for managing changes in the entity and field schema.
+ */
+interface ContentEntitySchemaManagerInterface {
+
+  /**
+   * Indicates that a definition has just been created.
+   *
+   * @var int
+   */
+  const DEFINITION_CREATED = 1;
+
+  /**
+   * Indicates that a definition has changes.
+   *
+   * @var int
+   */
+  const DEFINITION_UPDATED = 2;
+
+  /**
+   * Indicates that a definition has just been deleted.
+   *
+   * @var int
+   */
+  const DEFINITION_DELETED = 3;
+
+  /**
+   * Returns a list of changes to entity type and field storage definitions.
+   *
+   * @param string $entity_type_id
+   *   (optional) The identifier of the entity type to be checked for changes.
+   *   Defaults to all the available entity types.
+   *
+   * @return array
+   *   An associative array keyed by entity type id of change descriptors. Every
+   *   entry is an associative array with the following optional keys:
+   *   - entity_type: a scalar having only the ENTITY_TYPE_UPDATED value.
+   *   - field_storage_definitions: an associative array keyed by field name of
+   *     scalars having one value among FIELD_STORAGE_DEFINITION_CREATED,
+   *     FIELD_STORAGE_DEFINITION_UPDATED, FIELD_STORAGE_DEFINITION_DELETED.
+   *   - data_migration: boolean indicating whether the changes imply a data
+   *     migration.
+   */
+  public function getChangeList($entity_type_id = NULL);
+
+  /**
+   * Applies all the detected valid changes.
+   *
+   * Only changes that do not imply a data migration are applied when data is
+   * available for a certain entity type. If any change fails to comply with
+   * this policy the operation is aborted.
+   *
+   * @param string $entity_type_id
+   *   (optional) The identifier of the entity type whose changes have to be
+   *   applied. Defaults to all the available entity types.
+   *
+   * @throws \Drupal\Core\Entity\EntityStorageException
+   *   This exception is thrown when trying to apply changes that imply a data
+   *   migration, when data is available in the storage.
+   */
+  public function applyChanges($entity_type_id = NULL);
+
+  /**
+   * Returns a human readable summary of the detected changes.
+   *
+   * @param string $entity_type_id
+   *   (optional) The identifier of the entity type to be checked for changes.
+   *   Defaults to all the available entity types.
+   */
+  public function getChangeSummary($entity_type_id = NULL);
+
+  /**
+   * Returns a requirements array describing the current status.
+   *
+   * @param string $phase
+   *   The phase identifier.
+   *
+   * @return array
+   *   A requirements array.
+   *
+   * @see hook_requirements()
+   */
+  public function getSystemRequirements($phase);
+
+  /**
+   * Reacts to the creation of an entity type definition.
+   *
+   * @param ContentEntityTypeInterface $definition
+   *   The entity type definition.
+   */
+  public function onEntityTypeDefinitionCreate(ContentEntityTypeInterface $definition);
+
+  /**
+   * Reacts to the update of an entity type definition.
+   *
+   * @param ContentEntityTypeInterface $definition
+   *   The updated entity type definition.
+   * @param ContentEntityTypeInterface $original
+   *   The original entity type definition.
+   */
+  public function onEntityTypeDefinitionUpdate(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original);
+
+  /**
+   * Reacts to the deletion of an entity type definition.
+   *
+   * @param ContentEntityTypeInterface $definition
+   *   The entity type definition.
+   */
+  public function onEntityTypeDefinitionDelete(ContentEntityTypeInterface $definition);
+
+  /**
+   * Reacts to the creation of a field storage definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
+   *   The field storage definition.
+   */
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $definition);
+
+  /**
+   * Reacts to the update of a field storage definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
+   *   The updated field storage definition.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original field storage definition.
+   */
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original);
+
+  /**
+   * Reacts to the deletion of a field storage definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
+   *   The field storage definition.
+   */
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $definition);
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaProviderInterface.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaProviderInterface.php
new file mode 100644
index 0000000..d1df87d
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaProviderInterface.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\ContentEntitySchemaProviderInterface.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
+/**
+ * Provides a common interface for a storage classes providing entity schema.
+ */
+interface ContentEntitySchemaProviderInterface {
+
+  /**
+   * Checks whether the definition changes imply entity schema changes.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $definition
+   *   The updated entity type definition.
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $original
+   *   The original entity type definition.
+   *
+   * @return bool
+   *   TRUE if the changes imply entity schema changes, FALSE otherwise.
+   */
+  public function requiresEntitySchemaChanges(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original);
+
+  /**
+   * Checks whether the definition changes imply field schema changes.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
+   *   The updated field storage definition.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original field storage definition.
+   *
+   * @return bool
+   *   TRUE if the changes imply field schema changes, FALSE otherwise.
+   */
+  public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original);
+
+  /**
+   * Checks whether the entity type definition changes imply a data migration.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $definition
+   *   The updated entity type definition.
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $original
+   *   The original entity type definition.
+   *
+   * @return bool
+   *   TRUE if the changes imply a data migration, FALSE otherwise.
+   */
+  public function requiresEntityDataMigration(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original);
+
+
+  /**
+   * Checks whether the field storage definition changes imply a data migration.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
+   *   The updated field storage definition.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original field storage definition.
+   *
+   * @return bool
+   *   TRUE if the changes imply a data migration, FALSE otherwise.
+   */
+  public function requiresFieldDataMigration(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original);
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php
deleted file mode 100644
index a38b82c..0000000
--- a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Entity\Schema\EntitySchemaHandlerInterface.
- */
-
-namespace Drupal\Core\Entity\Schema;
-
-/**
- * Defines an interface for handling the storage schema of entities.
- */
-interface EntitySchemaHandlerInterface extends EntitySchemaProviderInterface {
-}
diff --git a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaProviderInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaProviderInterface.php
deleted file mode 100644
index c976782..0000000
--- a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaProviderInterface.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Entity\Schema\EntitySchemaProviderInterface.
- */
-
-namespace Drupal\Core\Entity\Schema;
-
-/**
- * Defines a common interface to return the storage schema for entities.
- */
-interface EntitySchemaProviderInterface {
-
-  /**
-   * Gets the full schema array for a given entity type.
-   *
-   * @return array
-   *   A schema array for the entity type's tables.
-   */
-  public function getSchema();
-
-}
diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
index e872d57..f950887 100644
--- a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
+++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
@@ -7,19 +7,28 @@
 
 namespace Drupal\Core\Entity\Sql;
 
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
 /**
  * Defines a default table mapping class.
  */
-class DefaultTableMapping implements TableMappingInterface {
+class DefaultTableMapping implements DefaultTableMappingInterface {
 
   /**
-   * A list of field storage definitions that are available for this mapping.
+   * The field storage definitions of this mapping.
    *
    * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
    */
   protected $fieldStorageDefinitions = array();
 
   /**
+   * The base field definitions of this mapping.
+   *
+   * @var \Drupal\Core\Field\FieldDefinitionInterface[]
+   */
+  protected $baseFieldDefinitions = array();
+
+  /**
    * A list of field names per table.
    *
    * This corresponds to the return value of
@@ -74,11 +83,13 @@ class DefaultTableMapping implements TableMappingInterface {
    * Constructs a DefaultTableMapping.
    *
    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
-   *   A list of field storage definitions that should be available for the
-   *   field columns of this table mapping.
+   *   The field storage definitions for which to create the table mapping.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface[] $base_field_definitions
+   *   The base field definitions for which to create the table mapping.
    */
-  public function __construct(array $storage_definitions) {
+  public function __construct(array $storage_definitions, array $base_field_definitions) {
     $this->fieldStorageDefinitions = $storage_definitions;
+    $this->baseFieldDefinitions = $base_field_definitions;
   }
 
   /**
@@ -99,7 +110,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];
   }
@@ -177,4 +197,109 @@ public function setExtraColumns($table_name, array $column_names) {
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
+     return !$storage_definition->hasCustomStorage() && isset($this->baseFieldDefinitions[$storage_definition->getName()]) && !$storage_definition->isMultiple();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresDedicatedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
+    return !$storage_definition->hasCustomStorage() && !$this->allowsSharedTableStorage($storage_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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}
+   */
+  public function getReservedColumns() {
+    return array('deleted');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDedicatedDataTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) {
+    if ($is_deleted) {
+      // When a field is a deleted, the table is renamed to
+      // {field_deleted_data_FIELD_UUID}. To make sure we don't end up with
+      // table names longer than 64 characters, we hash the unique storage
+      // identifier and return the first 10 characters so we end up with a short
+      // unique ID.
+      return "field_deleted_data_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
+    }
+    else {
+      return $this->generateFieldTableName($storage_definition, FALSE);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDedicatedRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) {
+    if ($is_deleted) {
+      // When a field is a deleted, the table is renamed to
+      // {field_deleted_revision_FIELD_UUID}. To make sure we don't end up with
+      // table names longer than 64 characters, we hash the unique storage
+      // identifier and return the first 10 characters so we end up with a short
+      // unique ID.
+      return "field_deleted_revision_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
+    }
+    else {
+      return $this->generateFieldTableName($storage_definition, TRUE);
+    }
+  }
+
+  /**
+   * Generates a safe and unambiguous field table name.
+   *
+   * The method accounts for a maximum table name length of 64 characters, and
+   * takes care of disambiguation.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   * @param bool $revision
+   *   TRUE for revision table, FALSE otherwise.
+   *
+   * @return string
+   *   The final table name.
+   */
+  protected function generateFieldTableName(FieldStorageDefinitionInterface $storage_definition, $revision) {
+    $separator = $revision ? '_revision__' : '__';
+    $table_name = $storage_definition->getTargetEntityTypeId() . $separator . $storage_definition->getName();
+    // Limit the string to 48 characters, keeping a 16 characters margin for db
+    // prefixes.
+    if (strlen($table_name) > 48) {
+      // Use a shorter separator, a truncated entity_type, and a hash of the
+      // field UUID.
+      $separator = $revision ? '_r__' : '__';
+      // Truncate to the same length for the current and revision tables.
+      $entity_type = substr($storage_definition->getTargetEntityTypeId(), 0, 34);
+      $field_hash = substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
+      $table_name = $entity_type . $separator . $field_hash;
+    }
+    return $table_name;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldColumnName(FieldStorageDefinitionInterface $storage_definition, $column) {
+    return in_array($column, $this->getReservedColumns()) ? $column : $storage_definition->getName() . '_' . $column;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMappingInterface.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMappingInterface.php
new file mode 100644
index 0000000..26b58a1
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMappingInterface.php
@@ -0,0 +1,118 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Sql\DefaultTableMappingInterface.
+ */
+
+namespace Drupal\Core\Entity\Sql;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
+/**
+ * Provides an common interface for mapping field columns to SQL tables.
+ */
+interface DefaultTableMappingInterface extends TableMappingInterface {
+
+  /**
+   * Returns a list of dedicated table names for this mapping.
+   *
+   * @return string[]
+   *   An array of table names.
+   */
+  public function getDedicatedTableNames();
+
+  /**
+   * Checks whether the given field can be stored in a shared table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   *
+   * @return bool
+   *   TRUE if the field can be stored in a dedicated table, FALSE otherwise.
+   */
+  public function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition);
+
+  /**
+   * 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);
+
+  /**
+   * A list of columns that can not be used as field type columns.
+   *
+   * @return array
+   */
+  public function getReservedColumns();
+
+  /**
+   * Generates a table name for a field data table.
+   *
+   * @private Calling this function circumvents the entity system and is
+   * strongly discouraged. This function is not considered part of the public
+   * API and modules relying on it might break even in minor releases. Only
+   * call this function to write a query that \Drupal::entityQuery() does not
+   * support. Always call entity_load() before using the data found in the
+   * table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   * @param bool $is_deleted
+   *   (optional) Whether the table name holding the values of a deleted field
+   *   should be returned.
+   *
+   * @return string
+   *   A string containing the generated name for the database table.
+   */
+  public function getDedicatedDataTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE);
+
+  /**
+   * Generates a table name for a field revision archive table.
+   *
+   * @private Calling this function circumvents the entity system and is
+   * strongly discouraged. This function is not considered part of the public
+   * API and modules relying on it might break even in minor releases. Only
+   * call this function to write a query that \Drupal::entityQuery() does not
+   * support. Always call entity_load() before using the data found in the
+   * table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   * @param bool $is_deleted
+   *   (optional) Whether the table name holding the values of a deleted field
+   *   should be returned.
+   *
+   * @return string
+   *   A string containing the generated name for the database table.
+   */
+  public function getDedicatedRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE);
+
+
+  /**
+   * Generates a column name for a field data table.
+   *
+   * @private Calling this function circumvents the entity system and is
+   * strongly discouraged. This function is not considered part of the public
+   * API and modules relying on it might break even in minor releases. Only
+   * call this function to write a query that \Drupal::entityQuery() does not
+   * support. Always call entity_load() before using the data found in the
+   * table.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition.
+   * @param string $column
+   *   The name of the column.
+   *
+   * @return string
+   *   A string containing a generated column name for a field data table that is
+   *   unique among all other fields.
+   */
+  public function getFieldColumnName(FieldStorageDefinitionInterface $storage_definition, $column);
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php
index 8c4e3d8..02174bf 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php
@@ -8,19 +8,22 @@
 namespace Drupal\Core\Entity\Sql;
 
 use Drupal\Core\Entity\EntityStorageInterface;
-use Drupal\Core\Entity\Schema\EntitySchemaProviderInterface;
 
 /**
  * A common interface for SQL-based storage controllers.
  */
-interface SqlEntityStorageInterface extends EntityStorageInterface, EntitySchemaProviderInterface {
+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 eac7318..a703ae2 100644
--- a/core/lib/Drupal/Core/Extension/ModuleHandler.php
+++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
@@ -12,7 +12,7 @@
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\String;
 use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Entity\Schema\EntitySchemaProviderInterface;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -845,19 +845,12 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
         }
         drupal_set_installed_schema_version($module, $version);
 
+        /** @var \Drupal\Core\Entity\Schema\ContentEntitySchemaManagerInterface $entity_schema_manager */
+        $entity_schema_manager = \Drupal::service('entity.schema.manager');
         // Install any entity schemas belonging to the module.
-        $entity_manager = \Drupal::entityManager();
-        $schema = \Drupal::database()->schema();
-        foreach ($entity_manager->getDefinitions() as $entity_type) {
-          if ($entity_type->getProvider() == $module) {
-            $storage = $entity_manager->getStorage($entity_type->id());
-            if ($storage instanceof EntitySchemaProviderInterface) {
-              foreach ($storage->getSchema() as $table_name => $table_schema) {
-                if (!$schema->tableExists($table_name)) {
-                  $schema->createTable($table_name, $table_schema);
-                }
-              }
-            }
+        foreach (\Drupal::entityManager()->getDefinitions() as $entity_type) {
+          if ($entity_type instanceof ContentEntityTypeInterface && $entity_type->getProvider() == $module) {
+            $entity_schema_manager->onEntityTypeDefinitionCreate($entity_type);
           }
         }
 
@@ -953,19 +946,12 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
       // Remove all configuration belonging to the module.
       \Drupal::service('config.manager')->uninstall('module', $module);
 
+      /** @var \Drupal\Core\Entity\Schema\ContentEntitySchemaManagerInterface $entity_schema_manager */
+      $entity_schema_manager = \Drupal::service('entity.schema.manager');
       // Remove any entity schemas belonging to the module.
-      $entity_manager = \Drupal::entityManager();
-      $schema = \Drupal::database()->schema();
-      foreach ($entity_manager->getDefinitions() as $entity_type) {
-        if ($entity_type->getProvider() == $module) {
-          $storage = $entity_manager->getStorage($entity_type->id());
-          if ($storage instanceof EntitySchemaProviderInterface) {
-            foreach ($storage->getSchema() as $table_name => $table_schema) {
-              if ($schema->tableExists($table_name)) {
-                $schema->dropTable($table_name);
-              }
-            }
-          }
+      foreach (\Drupal::entityManager()->getDefinitions() as $entity_type) {
+        if ($entity_type instanceof ContentEntityTypeInterface && $entity_type->getProvider() == $module) {
+          $entity_schema_manager->onEntityTypeDefinitionDelete($entity_type);
         }
       }
 
diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
index 48a5a4b..e4a4406 100644
--- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
+++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
@@ -11,7 +11,6 @@
 use Drupal\Core\Field\Entity\BaseFieldOverride;
 use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
 use Drupal\Core\TypedData\ListDataDefinition;
-use Drupal\field\FieldException;
 
 /**
  * A class for defining entity fields.
@@ -554,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'];
@@ -584,15 +578,6 @@ public function getColumns() {
   }
 
   /**
-   * A list of columns that can not be used as field type columns.
-   *
-   * @return array
-   */
-  public static function getReservedColumns() {
-    return array('deleted');
-  }
-
-  /**
    * {@inheritdoc}
    */
   public function hasCustomStorage() {
diff --git a/core/lib/Drupal/Core/Field/FieldException.php b/core/lib/Drupal/Core/Field/FieldException.php
new file mode 100644
index 0000000..cd1ecda
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/FieldException.php
@@ -0,0 +1,16 @@
+<?php
+
+/*
+ * @file
+ * Contains \Drupal\Core\Field\FieldException.
+ */
+
+namespace Drupal\Core\Field;
+
+/**
+ * Base class for all exceptions thrown by the Entity Field API functions.
+ *
+ * This class has no functionality of its own other than allowing all
+ * Entity Field API exceptions to be caught by a single catch block.
+ */
+class FieldException extends \RuntimeException {}
diff --git a/core/lib/Drupal/Core/Update/Form/UpdateEntitySchemaForm.php b/core/lib/Drupal/Core/Update/Form/UpdateEntitySchemaForm.php
new file mode 100644
index 0000000..6791f37
--- /dev/null
+++ b/core/lib/Drupal/Core/Update/Form/UpdateEntitySchemaForm.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Update\Form\UpdateEntitySchemaForm.
+ */
+
+namespace Drupal\Core\Update\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides the list of available database module updates.
+ */
+class UpdateEntitySchemaForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'update_entity_schema_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    // Build a summary of the entity schema changes.
+    $summary = \Drupal::service('entity.schema.manager')->getChangeSummary();
+    if ($summary) {
+      $entity_manager = \Drupal::entityManager();
+      foreach ($summary as $entity_type_id => $items) {
+        $definition = $entity_manager->getDefinition($entity_type_id);
+        $form['summary'][$entity_type_id] = array(
+          '#type' => 'details',
+          '#title' => $definition->getLabel(),
+        );
+        $form['summary'][$entity_type_id]['changes'] = array(
+          '#theme' => 'item_list',
+          '#items' => $items,
+        );
+      }
+    }
+    else {
+      $form['summary'] = array('#markup' => $this->t('No entity schema changes available.'));
+    }
+
+    $form['op'] = array(
+      '#type' => 'hidden',
+      '#value' => 'selection',
+    );
+
+    $form['entity_schema_updates'] = array(
+      '#type' => 'hidden',
+      '#value' => 1,
+    );
+
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Continue'),
+      '#button_type' => 'primary',
+      // This is necessary to use the hidden element to determine the next op.
+      '#name' => 'submit',
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php b/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php
index 574ec6c..fad0616 100644
--- a/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php
+++ b/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
 
 /**
  * Provides the list of available database module updates.
@@ -25,7 +26,7 @@ public function getFormID() {
   /**
    * {@inheritdoc}
    */
-  public function buildForm(array $form, FormStateInterface $form_state) {
+  public function buildForm(array $form, FormStateInterface $form_state, $force_updates = FALSE) {
     $count = 0;
     $incompatible_count = 0;
     $form['start'] = array(
@@ -88,7 +89,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       drupal_set_message('Some of the pending updates cannot be applied because their dependencies were not met.', 'warning');
     }
 
-    if (empty($count)) {
+    if (empty($count) && !$force_updates) {
       drupal_set_message(t('No pending updates.'));
       unset($form);
       $form['links'] = array(
@@ -100,20 +101,29 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       update_flush_all_caches();
     }
     else {
-      $form['help'] = array(
-        '#markup' => '<p>The version of Drupal you are updating from has been automatically detected.</p>',
-        '#weight' => -5,
-      );
-      if ($incompatible_count) {
-        $form['start']['#title'] = format_plural(
-          $count,
-          '1 pending update (@number_applied to be applied, @number_incompatible skipped)',
-          '@count pending updates (@number_applied to be applied, @number_incompatible skipped)',
-          array('@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count)
+      if ($count > 0) {
+        $form['help'] = array(
+          '#markup' => '<p>The version of Drupal you are updating from has been automatically detected.</p>',
+          '#weight' => -5,
         );
+        if ($incompatible_count) {
+          $form['start']['#title'] = format_plural(
+            $count,
+            '1 pending update (@number_applied to be applied, @number_incompatible skipped)',
+            '@count pending updates (@number_applied to be applied, @number_incompatible skipped)',
+            array('@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count)
+          );
+        }
+        else {
+          $form['start']['#title'] = format_plural($count, '1 pending update', '@count pending updates');
+        }
       }
       else {
-        $form['start']['#title'] = format_plural($count, '1 pending update', '@count pending updates');
+        unset($form);
+        $form['help'] = array(
+          '#markup' => '<p>No module update available.</p>',
+          '#weight' => -5,
+        );
       }
       $form['actions'] = array('#type' => 'actions');
       $form['actions']['submit'] = array(
diff --git a/core/modules/aggregator/src/FeedSchemaHandler.php b/core/modules/aggregator/src/FeedSchemaHandler.php
new file mode 100644
index 0000000..d78a0c6
--- /dev/null
+++ b/core/modules/aggregator/src/FeedSchemaHandler.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\aggregator\FeedSchemaHandler.
+ */
+
+namespace Drupal\aggregator;
+
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler;
+
+/**
+ * Defines the feed schema handler.
+ */
+class FeedSchemaHandler extends ContentEntitySchemaHandler {
+
+  /**
+   * {@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'),
+    );
+
+    return $schema;
+  }
+
+}
diff --git a/core/modules/aggregator/src/FeedStorage.php b/core/modules/aggregator/src/FeedStorage.php
index da784f5..03d4716 100644
--- a/core/modules/aggregator/src/FeedStorage.php
+++ b/core/modules/aggregator/src/FeedStorage.php
@@ -21,24 +21,11 @@ class FeedStorage extends ContentEntityDatabaseStorage implements FeedStorageInt
   /**
    * {@inheritdoc}
    */
-  public function getSchema() {
-    $schema = parent::getSchema();
-
-    // 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'),
-    );
-
-    return $schema;
+  protected function schemaHandler() {
+    if (!isset($this->schemaHandler)) {
+      $this->schemaHandler = new FeedSchemaHandler($this->entityManager, $this->entityType, $this, $this->database);
+    }
+    return $this->schemaHandler;
   }
 
   /**
diff --git a/core/modules/aggregator/src/ItemSchemaHandler.php b/core/modules/aggregator/src/ItemSchemaHandler.php
new file mode 100644
index 0000000..3a22ac0
--- /dev/null
+++ b/core/modules/aggregator/src/ItemSchemaHandler.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\aggregator\ItemSchemaHandler.
+ */
+
+namespace Drupal\aggregator;
+
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler;
+
+/**
+ * Defines the item schema handler.
+ */
+class ItemSchemaHandler extends ContentEntitySchemaHandler {
+
+  /**
+   * {@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'),
+      ),
+    );
+
+    return $schema;
+  }
+
+}
diff --git a/core/modules/aggregator/src/ItemStorage.php b/core/modules/aggregator/src/ItemStorage.php
index f2b4aa6..9bc59c1 100644
--- a/core/modules/aggregator/src/ItemStorage.php
+++ b/core/modules/aggregator/src/ItemStorage.php
@@ -7,9 +7,8 @@
 
 namespace Drupal\aggregator;
 
-use Drupal\aggregator\Entity\Item;
-use Drupal\Core\Entity\Query\QueryInterface;
 use Drupal\Core\Entity\ContentEntityDatabaseStorage;
+use Drupal\Core\Entity\Query\QueryInterface;
 
 /**
  * Controller class for aggregators items.
@@ -22,24 +21,11 @@ class ItemStorage extends ContentEntityDatabaseStorage implements ItemStorageInt
   /**
    * {@inheritdoc}
    */
-  public function getSchema() {
-    $schema = parent::getSchema();
-
-    // 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'),
-      ),
-    );
-
-    return $schema;
+  protected function schemaHandler() {
+    if (!isset($this->schemaHandler)) {
+      $this->schemaHandler = new ItemSchemaHandler($this->entityManager, $this->entityType, $this, $this->database);
+    }
+    return $this->schemaHandler;
   }
 
   /**
diff --git a/core/modules/block_content/src/BlockContentSchemaHandler.php b/core/modules/block_content/src/BlockContentSchemaHandler.php
new file mode 100644
index 0000000..33ecce2
--- /dev/null
+++ b/core/modules/block_content/src/BlockContentSchemaHandler.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\block_content\BlockContentSchemaHandler.
+ */
+
+namespace Drupal\block_content;
+
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler;
+
+/**
+ * Defines the block content schema handler.
+ */
+class BlockContentSchemaHandler extends ContentEntitySchemaHandler {
+
+  /**
+   * {@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['block_content_field_data']['fields']['info']['not null'] = TRUE;
+
+    $schema['block_content_field_data']['unique keys'] += array(
+      'block_content__info' => array('info', 'langcode'),
+    );
+
+    return $schema;
+  }
+
+}
diff --git a/core/modules/block_content/src/BlockContentStorage.php b/core/modules/block_content/src/BlockContentStorage.php
index b98a861..b45c8eb 100644
--- a/core/modules/block_content/src/BlockContentStorage.php
+++ b/core/modules/block_content/src/BlockContentStorage.php
@@ -17,18 +17,11 @@ class BlockContentStorage extends ContentEntityDatabaseStorage {
   /**
    * {@inheritdoc}
    */
-  public function getSchema() {
-    $schema = parent::getSchema();
-
-    // 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'),
-    );
-
-    return $schema;
+  protected function schemaHandler() {
+    if (!isset($this->schemaHandler)) {
+      $this->schemaHandler = new BlockContentSchemaHandler($this->entityManager, $this->entityType, $this, $this->database);
+    }
+    return $this->schemaHandler;
   }
 
 }
diff --git a/core/modules/comment/src/CommentSchemaHandler.php b/core/modules/comment/src/CommentSchemaHandler.php
new file mode 100644
index 0000000..a69fc5a
--- /dev/null
+++ b/core/modules/comment/src/CommentSchemaHandler.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\comment\CommentSchemaHandler.
+ */
+
+namespace Drupal\comment;
+
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler;
+
+/**
+ * Defines the comment schema handler.
+ */
+class CommentSchemaHandler extends ContentEntitySchemaHandler {
+
+  /**
+   * {@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['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(
+        'entity_id',
+        'entity_type',
+        'comment_type',
+        'status',
+        'created',
+        'cid',
+        'thread',
+      ),
+      'comment__entity_langcode' => array(
+        'entity_id',
+        'entity_type',
+        '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;
+  }
+
+}
diff --git a/core/modules/comment/src/CommentStorage.php b/core/modules/comment/src/CommentStorage.php
index 9639d59..fccb642 100644
--- a/core/modules/comment/src/CommentStorage.php
+++ b/core/modules/comment/src/CommentStorage.php
@@ -9,11 +9,11 @@
 
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Database\Connection;
-use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\Core\Session\AccountInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -321,43 +321,11 @@ public function loadThread(EntityInterface $entity, $field_name, $mode, $comment
   /**
    * {@inheritdoc}
    */
-  public function getSchema() {
-    $schema = parent::getSchema();
-
-    // 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(
-        'entity_id',
-        'entity_type',
-        'comment_type',
-        'status',
-        'created',
-        'cid',
-        'thread',
-      ),
-      'comment__entity_langcode' => array(
-        'entity_id',
-        'entity_type',
-        '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;
+  protected function schemaHandler() {
+    if (!isset($this->schemaHandler)) {
+      $this->schemaHandler = new CommentSchemaHandler($this->entityManager, $this->entityType, $this, $this->database);
+    }
+    return $this->schemaHandler;
   }
 
   /**
diff --git a/core/modules/contact/src/Tests/Views/ContactFieldsTest.php b/core/modules/contact/src/Tests/Views/ContactFieldsTest.php
index 63c7d1a..a4afee7 100644
--- a/core/modules/contact/src/Tests/Views/ContactFieldsTest.php
+++ b/core/modules/contact/src/Tests/Views/ContactFieldsTest.php
@@ -7,7 +7,6 @@
 
 namespace Drupal\contact\Tests\Views;
 
-use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\views\Tests\ViewTestBase;
 
 /**
@@ -60,7 +59,7 @@ protected function setUp() {
   public function testViewsData() {
     // Test that the field is not exposed to views, since contact_message
     // entities have no storage.
-    $table_name = ContentEntityDatabaseStorage::_fieldTableName($this->field_storage);
+    $table_name = 'contact_message__' .  $this->field_storage->getName();
     $data = $this->container->get('views.views_data')->get($table_name);
     $this->assertFalse($data, 'The field is not exposed to Views.');
   }
diff --git a/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.install b/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.install
index bcbe5df..c749147 100644
--- a/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.install
+++ b/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.install
@@ -9,16 +9,10 @@
  * Implements hook_install().
  */
 function contact_storage_test_install() {
-  // ModuleHandler won't create the schema automatically because Message entity
-  // belongs to contact.module.
-  // @todo Remove this when https://www.drupal.org/node/1498720 is in.
+  // Recreate the original entity type definition, so we can trigger the entity
+  // schema creation.
   $entity_manager = \Drupal::entityManager();
-  $schema = \Drupal::database()->schema();
-  $entity_type = $entity_manager->getDefinition('contact_message');
-  $storage = $entity_manager->getStorage($entity_type->id());
-  foreach ($storage->getSchema() as $table_name => $table_schema) {
-    if (!$schema->tableExists($table_name)) {
-      $schema->createTable($table_name, $table_schema);
-    }
-  }
+  $original = clone $entity_manager->getDefinition('contact_message');
+  $original->setStorageClass('Drupal\Core\Entity\ContentEntityNullStorage');
+  $entity_manager->getStorage($original->id())->onEntityTypeDefinitionUpdate($original);
 }
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 3ef9d22..e3352c4 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
-      // ContentEntityDatabaseStorage::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/entity_reference/entity_reference.views.inc b/core/modules/entity_reference/entity_reference.views.inc
index e35884b..4580bce 100644
--- a/core/modules/entity_reference/entity_reference.views.inc
+++ b/core/modules/entity_reference/entity_reference.views.inc
@@ -5,7 +5,6 @@
  * Provides views data for the entity_reference module.
  */
 
-use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\field\FieldStorageConfigInterface;
 
 /**
@@ -14,6 +13,7 @@
 function entity_reference_field_views_data(FieldStorageConfigInterface $field_storage) {
   $data = field_views_field_default_views_data($field_storage);
   $entity_manager = \Drupal::entityManager();
+  $table_mapping = $entity_manager->getStorage($field_storage->getTargetEntityTypeId())->getTableMapping();
   foreach ($data as $table_name => $table_data) {
     // Add a relationship to the target entity type.
     $target_entity_type_id = $field_storage->getSetting('target_type');
@@ -38,13 +38,16 @@ function entity_reference_field_views_data(FieldStorageConfigInterface $field_st
 
     // Provide a reverse relationship for the entity type that is referenced by
     // the field.
-    $pseudo_field_name = 'reverse__' . $field_storage->getTargetEntityTypeId() . '__' . $field_storage->getName();
+    $entity_type_id = $field_storage->getTargetEntityTypeId();
+    $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_storage->getName();
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+    $table_mapping = \Drupal::entityManager()->getStorage($entity_type_id)->getTableMapping();
     $data[$target_base_table][$pseudo_field_name]['relationship'] = array(
       'title' => t('@label using @field_name', $args),
       'help' => t('Relate each @label with a @field_name.', $args),
       'id' => 'entity_reverse',
       'field_name' => $field_storage->getName(),
-      'field table' => ContentEntityDatabaseStorage::_fieldTableName($field_storage),
+      'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
       'field field' => $field_storage->getName() . '_target_id',
       'base' => $target_entity_type->getBaseTable(),
       'base field' => $target_entity_type->getKey('id'),
diff --git a/core/modules/field/field.purge.inc b/core/modules/field/field.purge.inc
index a17e6f2..5cbbc27 100644
--- a/core/modules/field/field.purge.inc
+++ b/core/modules/field/field.purge.inc
@@ -7,7 +7,7 @@
 
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\field\FieldStorageConfigInterface;
-use Drupal\field\FieldException;
+use Drupal\Core\Field\FieldException;
 
 /**
  * @defgroup field_purge Field API bulk data deletion
diff --git a/core/modules/field/field.views.inc b/core/modules/field/field.views.inc
index 3f68419..95e360e 100644
--- a/core/modules/field/field.views.inc
+++ b/core/modules/field/field.views.inc
@@ -22,7 +22,7 @@ function field_views_data() {
   $module_handler = \Drupal::moduleHandler();
 
   foreach (\Drupal::entityManager()->getStorage('field_storage_config')->loadMultiple() as $field) {
-    if (_field_views_is_sql_entity_type($field)) {
+    if (_field_views_get_entity_type_storage($field)) {
       $result = (array) $module_handler->invoke($field->module, 'field_views_data', array($field));
       if (empty($result)) {
         $result = field_views_field_default_views_data($field);
@@ -48,7 +48,7 @@ function field_views_data() {
  */
 function field_views_data_alter(&$data) {
   foreach (\Drupal::entityManager()->getStorage('field_storage_config')->loadMultiple() as $field) {
-    if (_field_views_is_sql_entity_type($field)) {
+    if (_field_views_get_entity_type_storage($field)) {
       $function = $field->module . '_field_views_data_views_data_alter';
       if (function_exists($function)) {
         $function($data, $field);
@@ -63,12 +63,17 @@ function field_views_data_alter(&$data) {
  * @param \Drupal\field\FieldStorageConfigInterface $field_storage
  *   The field storage definition.
  *
- * @return bool
- *   True if the entity type uses ContentEntityDatabaseStorage.
+ * @return \Drupal\Core\Entity\ContentEntityDatabaseStorage
+ *   Returns the entity type storage if supported.
  */
-function _field_views_is_sql_entity_type(FieldStorageConfigInterface $field_storage) {
+function _field_views_get_entity_type_storage(FieldStorageConfigInterface $field_storage) {
+  $result = FALSE;
   $entity_manager = \Drupal::entityManager();
-  return $entity_manager->hasDefinition($field_storage->entity_type) && $entity_manager->getStorage($field_storage->entity_type) instanceof ContentEntityDatabaseStorage;
+  if ($entity_manager->hasDefinition($field_storage->getTargetEntityTypeId())) {
+    $storage = $entity_manager->getStorage($field_storage->getTargetEntityTypeId());
+    $result = $storage instanceof ContentEntityDatabaseStorage ? $storage : FALSE;
+  }
+  return $result;
 }
 
 /**
@@ -120,6 +125,11 @@ function field_views_field_default_views_data(FieldStorageConfigInterface $field
   if (!$field_storage->getBundles()) {
     return $data;
   }
+  // Check whether the entity type storage is supported.
+  $storage = _field_views_get_entity_type_storage($field_storage);
+  if (!$storage) {
+    return $data;
+  }
 
   $field_name = $field_storage->getName();
   $field_columns = $field_storage->getColumns();
@@ -127,7 +137,7 @@ function field_views_field_default_views_data(FieldStorageConfigInterface $field
   // Grab information about the entity type tables.
   // We need to join to both the base table and the data table, if available.
   $entity_manager = \Drupal::entityManager();
-  $entity_type_id = $field_storage->entity_type;
+  $entity_type_id = $field_storage->getTargetEntityTypeId();
   $entity_type = $entity_manager->getDefinition($entity_type_id);
   if (!$base_table = $entity_type->getBaseTable()) {
     // We cannot do anything if for some reason there is no base table.
@@ -150,15 +160,18 @@ function field_views_field_default_views_data(FieldStorageConfigInterface $field
   }
 
   // Description of the field tables.
+  // @todo Generalize this code to make it work with any table layout. See
+  //   https://drupal.org/node/2079019.
+  $table_mapping = $storage->getTableMapping();
   $field_tables = array(
     EntityStorageInterface::FIELD_LOAD_CURRENT => array(
-      'table' => ContentEntityDatabaseStorage::_fieldTableName($field_storage),
+      'table' => $table_mapping->getDedicatedDataTableName($field_storage),
       'alias' => "{$entity_type_id}__{$field_name}",
     ),
   );
   if ($supports_revisions) {
     $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION] = array(
-      'table' => ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage),
+      'table' => $table_mapping->getDedicatedRevisionTableName($field_storage),
       'alias' => "{$entity_type_id}_revision__{$field_name}",
     );
   }
@@ -220,7 +233,7 @@ function field_views_field_default_views_data(FieldStorageConfigInterface $field
   // Build the list of additional fields to add to queries.
   $add_fields = array('delta', 'langcode', 'bundle');
   foreach (array_keys($field_columns) as $column) {
-    $add_fields[] = ContentEntityDatabaseStorage::_fieldColumnName($field_storage, $column);
+    $add_fields[] = $table_mapping->getFieldColumnName($field_storage, $column);
   }
   // Determine the label to use for the field. We don't have a label available
   // at the field level, so we just go through all instances and take the one
@@ -344,11 +357,10 @@ function field_views_field_default_views_data(FieldStorageConfigInterface $field
       else {
         $group = t('@group (historical data)', array('@group' => $group_name));
       }
-      $column_real_name = ContentEntityDatabaseStorage::_fieldColumnName($field_storage, $column);
+      $column_real_name = $table_mapping->getFieldColumnName($field_storage, $column);
 
       // Load all the fields from the table by default.
-      $field_sql_schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field_storage);
-      $additional_fields = array_keys($field_sql_schema[$table]['fields']);
+      $additional_fields = $table_mapping->getAllColumns($table);
 
       $data[$table_alias][$column_real_name] = array(
         'group' => $group,
diff --git a/core/modules/field/src/Entity/FieldInstanceConfig.php b/core/modules/field/src/Entity/FieldInstanceConfig.php
index 23a4dc8..0ae84ad 100644
--- a/core/modules/field/src/Entity/FieldInstanceConfig.php
+++ b/core/modules/field/src/Entity/FieldInstanceConfig.php
@@ -10,7 +10,7 @@
 use Drupal\Component\Utility\String;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Field\FieldConfigBase;
-use Drupal\field\FieldException;
+use Drupal\Core\Field\FieldException;
 use Drupal\field\FieldStorageConfigInterface;
 use Drupal\field\FieldInstanceConfigInterface;
 
@@ -128,7 +128,7 @@ public function postCreate(EntityStorageInterface $storage) {
   /**
    * Overrides \Drupal\Core\Entity\Entity::preSave().
    *
-   * @throws \Drupal\field\FieldException
+   * @throws \Drupal\Core\FieldException
    *   If the field instance definition is invalid.
    * @throws \Drupal\Core\Entity\EntityStorageException
    *   In case of failures at the configuration storage level.
diff --git a/core/modules/field/src/Entity/FieldStorageConfig.php b/core/modules/field/src/Entity/FieldStorageConfig.php
index 2c0e60b..2f86ddf 100644
--- a/core/modules/field/src/Entity/FieldStorageConfig.php
+++ b/core/modules/field/src/Entity/FieldStorageConfig.php
@@ -12,7 +12,7 @@
 use Drupal\Core\Config\Entity\ConfigEntityBase;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
-use Drupal\field\FieldException;
+use Drupal\Core\Field\FieldException;
 use Drupal\field\FieldStorageConfigInterface;
 
 /**
@@ -227,7 +227,7 @@ public function id() {
   /**
    * Overrides \Drupal\Core\Entity\Entity::preSave().
    *
-   * @throws \Drupal\field\FieldException
+   * @throws \Drupal\Core\FieldException
    *   If the field definition is invalid.
    * @throws \Drupal\Core\Entity\EntityStorageException
    *   In case of failures at the configuration storage level.
@@ -254,7 +254,7 @@ public function preSave(EntityStorageInterface $storage) {
    * @param \Drupal\Core\Entity\EntityStorageInterface $storage
    *   The entity storage.
    *
-   * @throws \Drupal\field\FieldException If the field definition is invalid.
+   * @throws \Drupal\Core\FieldException If the field definition is invalid.
    */
    protected function preSaveNew(EntityStorageInterface $storage) {
     $entity_manager = \Drupal::entityManager();
@@ -292,8 +292,8 @@ protected function preSaveNew(EntityStorageInterface $storage) {
     // definition is passed to the various hooks and written to config.
      $this->settings += $field_type_manager->getDefaultSettings($this->type);
 
-    // Notify the entity storage.
-    $entity_manager->getStorage($this->entity_type)->onFieldStorageDefinitionCreate($this);
+     // Notify the entity schema manager.
+     \Drupal::service('entity.schema.manager')->onFieldStorageDefinitionCreate($this);
   }
 
   /**
@@ -336,10 +336,10 @@ protected function preSaveUpdated(EntityStorageInterface $storage) {
     // invokes hook_field_storage_config_update_forbid().
     $module_handler->invokeAll('field_storage_config_update_forbid', array($this, $this->original));
 
-    // Notify the storage. The controller can reject the definition
+    // Notify the schema manager. The controller can reject the definition
     // update as invalid by raising an exception, which stops execution before
     // the definition is written to config.
-    $entity_manager->getStorage($this->entity_type)->onFieldStorageDefinitionUpdate($this, $this->original);
+    \Drupal::service('entity.schema.manager')->onFieldStorageDefinitionUpdate($this, $this->original);
   }
 
   /**
@@ -408,7 +408,7 @@ public static function postDelete(EntityStorageInterface $storage, array $fields
     // Notify the storage.
     foreach ($fields as $field) {
       if (!$field->deleted) {
-        \Drupal::entityManager()->getStorage($field->entity_type)->onFieldStorageDefinitionDelete($field);
+        \Drupal::service('entity.schema.manager')->onFieldStorageDefinitionDelete($field);
         $field->deleted = TRUE;
       }
     }
@@ -432,11 +432,6 @@ public function getSchema() {
         'foreign keys' => array(),
       );
 
-      // Check that the schema does not include forbidden column names.
-      if (array_intersect(array_keys($schema['columns']), static::getReservedColumns())) {
-        throw new FieldException(String::format('Illegal field type @field_type on @field_name.', array('@field_type' => $this->type, '@field_name' => $this->name)));
-      }
-
       // Merge custom indexes with those specified by the field type. Custom
       // indexes prevail.
       $schema['indexes'] = $this->indexes + $schema['indexes'];
@@ -616,15 +611,6 @@ public function isQueryable() {
   }
 
   /**
-   * A list of columns that can not be used as field type columns.
-   *
-   * @return array
-   */
-  public static function getReservedColumns() {
-    return array('deleted');
-  }
-
-  /**
    * Determines whether a field has any data.
    *
    * @return bool
diff --git a/core/modules/field/src/FieldException.php b/core/modules/field/src/FieldException.php
deleted file mode 100644
index e54fa39..0000000
--- a/core/modules/field/src/FieldException.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-/*
- * @file
- * Definition of Drupal\field\FieldExeption.
- */
-
-namespace Drupal\field;
-
-/**
- * Base class for all exceptions thrown by Field API functions.
- *
- * This class has no functionality of its own other than allowing all
- * Field API exceptions to be caught by a single catch block.
- */
-class FieldException extends \RuntimeException {}
diff --git a/core/modules/field/src/Plugin/views/field/Field.php b/core/modules/field/src/Plugin/views/field/Field.php
index 4705437..ff58077 100644
--- a/core/modules/field/src/Plugin/views/field/Field.php
+++ b/core/modules/field/src/Plugin/views/field/Field.php
@@ -9,7 +9,6 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\Xss;
-use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
@@ -353,9 +352,12 @@ public function clickSort($order) {
     }
 
     $this->ensureMyTable();
-    $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->definition['entity_type']);
+    $entity_type_id = $this->definition['entity_type'];
+    $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
     $field_storage = $field_storage_definitions[$this->definition['field_name']];
-    $column = ContentEntityDatabaseStorage::_fieldColumnName($field_storage, $this->options['click_sort_column']);
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+    $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping();
+    $column = $table_mapping->getFieldColumnName($field_storage, $this->options['click_sort_column']);
     if (!isset($this->aliases[$column])) {
       // Column is not in query; add a sort on it (without adding the column).
       $this->aliases[$column] = $this->tableAlias . '.' . $column;
diff --git a/core/modules/field/src/Tests/BulkDeleteTest.php b/core/modules/field/src/Tests/BulkDeleteTest.php
index ee099a8..30d8e4b 100644
--- a/core/modules/field/src/Tests/BulkDeleteTest.php
+++ b/core/modules/field/src/Tests/BulkDeleteTest.php
@@ -7,7 +7,6 @@
 
 namespace Drupal\field\Tests;
 
-use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\field\Entity\FieldInstanceConfig;
 
@@ -178,11 +177,13 @@ function testDeleteFieldInstance() {
     $this->assertEqual($instance->bundle, $bundle, 'The deleted instance is for the correct bundle');
 
     // Check that the actual stored content did not change during delete.
-    $schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field_storage);
-    $table = ContentEntityDatabaseStorage::_fieldTableName($field_storage);
-    $column = ContentEntityDatabaseStorage::_fieldColumnName($field_storage, 'value');
+    $storage = \Drupal::entityManager()->getStorage($this->entity_type);
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+    $table_mapping = $storage->getTableMapping();
+    $table = $table_mapping->getDedicatedDataTableName($field_storage);
+    $column = $table_mapping->getFieldColumnName($field_storage, 'value');
     $result = db_select($table, 't')
-      ->fields('t', array_keys($schema[$table]['fields']))
+      ->fields('t')
       ->execute();
     foreach ($result as $row) {
       $this->assertEqual($this->entities[$row->entity_id]->{$field_storage->name}->value, $row->$column);
diff --git a/core/modules/field/src/Tests/CrudTest.php b/core/modules/field/src/Tests/CrudTest.php
index 1feb64f..2440640 100644
--- a/core/modules/field/src/Tests/CrudTest.php
+++ b/core/modules/field/src/Tests/CrudTest.php
@@ -10,7 +10,7 @@
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\field\Entity\FieldStorageConfig;
-use Drupal\field\FieldException;
+use Drupal\Core\Field\FieldException;
 
 /**
  * Tests field storage create, read, update, and delete.
diff --git a/core/modules/field/src/Tests/FieldDataCountTest.php b/core/modules/field/src/Tests/FieldDataCountTest.php
index 1a57fc9..66e60db 100644
--- a/core/modules/field/src/Tests/FieldDataCountTest.php
+++ b/core/modules/field/src/Tests/FieldDataCountTest.php
@@ -76,7 +76,8 @@ public function testEntityCountAndHasData() {
     $storage = \Drupal::entityManager()->getStorage('entity_test');
     if ($storage instanceof ContentEntityDatabaseStorage) {
       // Count the actual number of rows in the field table.
-      $field_table_name = $storage->_fieldTableName($field_storage);
+      $table_mapping = $storage->getTableMapping();
+      $field_table_name = $table_mapping->getDedicatedDataTableName($field_storage);
       $result = db_select($field_table_name, 't')
         ->fields('t')
         ->countQuery()
diff --git a/core/modules/field/src/Tests/FieldInstanceCrudTest.php b/core/modules/field/src/Tests/FieldInstanceCrudTest.php
index 495d989..769eb85 100644
--- a/core/modules/field/src/Tests/FieldInstanceCrudTest.php
+++ b/core/modules/field/src/Tests/FieldInstanceCrudTest.php
@@ -10,7 +10,7 @@
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\field\Entity\FieldInstanceConfig;
-use Drupal\field\FieldException;
+use Drupal\Core\Field\FieldException;
 
 /**
  * Create field entities by attaching fields to entities.
diff --git a/core/modules/field/src/Tests/Views/ApiDataTest.php b/core/modules/field/src/Tests/Views/ApiDataTest.php
index 7631e29..5ad8a53 100644
--- a/core/modules/field/src/Tests/Views/ApiDataTest.php
+++ b/core/modules/field/src/Tests/Views/ApiDataTest.php
@@ -6,7 +6,6 @@
  */
 
 namespace Drupal\field\Tests\Views;
-use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 
 /**
  * Tests the Field Views data.
@@ -51,8 +50,10 @@ function testViewsData() {
     // Check the table and the joins of the first field.
     // Attached to node only.
     $field_storage = $this->fieldStorages[0];
-    $current_table = ContentEntityDatabaseStorage::_fieldTableName($field_storage);
-    $revision_table = ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage);
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+    $table_mapping = \Drupal::entityManager()->getStorage('node')->getTableMapping();
+    $current_table = $table_mapping->getDedicatedDataTableName($field_storage);
+    $revision_table = $table_mapping->getDedicatedRevisionTableName($field_storage);
     $data[$current_table] = $views_data->get($current_table);
     $data[$revision_table] = $views_data->get($revision_table);
 
diff --git a/core/modules/file/file.views.inc b/core/modules/file/file.views.inc
index bfa3d15..eeb16fd 100644
--- a/core/modules/file/file.views.inc
+++ b/core/modules/file/file.views.inc
@@ -5,7 +5,6 @@
  * Provide views data for file.module.
  */
 
-use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\field\FieldStorageConfigInterface;
 
 /**
@@ -39,9 +38,12 @@ function file_field_views_data(FieldStorageConfigInterface $field_storage) {
  */
 function file_field_views_data_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) {
   $entity_type_id = $field_storage->entity_type;
-  $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id);
+  $entity_manager = \Drupal::entityManager();
+  $entity_type = $entity_manager->getDefinition($entity_type_id);
   $field_name = $field_storage->getName();
   $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
+  /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+  $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping();
 
   list($label) = field_views_field_label($entity_type_id, $field_name);
 
@@ -51,7 +53,7 @@ function file_field_views_data_views_data_alter(array &$data, FieldStorageConfig
     'id' => 'entity_reverse',
     'field_name' => $field_name,
     'entity_type' => $entity_type_id,
-    'field table' => ContentEntityDatabaseStorage::_fieldTableName($field_storage),
+    'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
     'field field' => $field_name . '_target_id',
     'base' => $entity_type->getBaseTable(),
     'base field' => $entity_type->getKey('id'),
diff --git a/core/modules/file/src/FileStorage.php b/core/modules/file/src/FileSchemaHandler.php
similarity index 53%
copy from core/modules/file/src/FileStorage.php
copy to core/modules/file/src/FileSchemaHandler.php
index 93dae42..67c7c57 100644
--- a/core/modules/file/src/FileStorage.php
+++ b/core/modules/file/src/FileSchemaHandler.php
@@ -2,36 +2,24 @@
 
 /**
  * @file
- * Definition of Drupal\file\FileStorage.
+ * Contains \Drupal\file\FileSchemaHandler.
  */
 
 namespace Drupal\file;
 
-use Drupal\Core\Entity\ContentEntityDatabaseStorage;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler;
 
 /**
- * File storage for files.
+ * Defines the file schema handler.
  */
-class FileStorage extends ContentEntityDatabaseStorage implements FileStorageInterface {
+class FileSchemaHandler extends ContentEntitySchemaHandler {
 
   /**
    * {@inheritdoc}
    */
-  public function spaceUsed($uid = NULL, $status = FILE_STATUS_PERMANENT) {
-    $query = $this->database->select($this->entityType->getBaseTable(), 'f')
-      ->condition('f.status', $status);
-    $query->addExpression('SUM(f.filesize)', 'filesize');
-    if (isset($uid)) {
-      $query->condition('f.uid', $uid);
-    }
-    return $query->execute()->fetchField();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getSchema() {
-    $schema = parent::getSchema();
+  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.
diff --git a/core/modules/file/src/FileStorage.php b/core/modules/file/src/FileStorage.php
index 93dae42..b1caea2 100644
--- a/core/modules/file/src/FileStorage.php
+++ b/core/modules/file/src/FileStorage.php
@@ -30,26 +30,11 @@ public function spaceUsed($uid = NULL, $status = FILE_STATUS_PERMANENT) {
   /**
    * {@inheritdoc}
    */
-  public function getSchema() {
-    $schema = parent::getSchema();
-
-    // 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'),
-    );
-
-    return $schema;
+  protected function schemaHandler() {
+    if (!isset($this->schemaHandler)) {
+      $this->schemaHandler = new FileSchemaHandler($this->entityManager, $this->entityType, $this, $this->database);
+    }
+    return $this->schemaHandler;
   }
 
 }
diff --git a/core/modules/image/image.views.inc b/core/modules/image/image.views.inc
index 5120350..04c32b9 100644
--- a/core/modules/image/image.views.inc
+++ b/core/modules/image/image.views.inc
@@ -5,7 +5,6 @@
  * Provide views data for image.module.
  */
 
-use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\field\FieldStorageConfigInterface;
 
 /**
@@ -37,10 +36,13 @@ function image_field_views_data(FieldStorageConfigInterface $field_storage) {
  * Views integration to provide reverse relationships on image fields.
  */
 function image_field_views_data_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) {
-  $entity_type_id = $field_storage->entity_type;
+  $entity_type_id = $field_storage->getTargetEntityTypeId();
   $field_name = $field_storage->getName();
-  $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id);
+  $entity_manager = \Drupal::entityManager();
+  $entity_type = $entity_manager->getDefinition($entity_type_id);
   $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
+  /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+  $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping();
 
   list($label) = field_views_field_label($entity_type_id, $field_name);
 
@@ -50,7 +52,7 @@ function image_field_views_data_views_data_alter(array &$data, FieldStorageConfi
     'id' => 'entity_reverse',
     'field_name' => $field_name,
     'entity_type' => $entity_type_id,
-    'field table' => ContentEntityDatabaseStorage::_fieldTableName($field_storage),
+    'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
     'field field' => $field_name . '_target_id',
     'base' => $entity_type->getBaseTable(),
     'base field' => $entity_type->getKey('id'),
diff --git a/core/modules/node/src/NodeStorage.php b/core/modules/node/src/NodeSchemaHandler.php
similarity index 54%
copy from core/modules/node/src/NodeStorage.php
copy to core/modules/node/src/NodeSchemaHandler.php
index 86cf982..ce1a3db 100644
--- a/core/modules/node/src/NodeStorage.php
+++ b/core/modules/node/src/NodeSchemaHandler.php
@@ -2,68 +2,24 @@
 
 /**
  * @file
- * Contains \Drupal\node\NodeStorage.
+ * Contains \Drupal\node\NodeSchemaHandler.
  */
 
 namespace Drupal\node;
 
-use Drupal\Core\Entity\ContentEntityDatabaseStorage;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler;
 
 /**
- * Defines the controller class for nodes.
- *
- * This extends the base storage class, adding required special handling for
- * node entities.
+ * Defines the node schema handler.
  */
-class NodeStorage extends ContentEntityDatabaseStorage implements NodeStorageInterface {
+class NodeSchemaHandler extends ContentEntitySchemaHandler {
 
   /**
    * {@inheritdoc}
    */
-  public function revisionIds(NodeInterface $node) {
-    return $this->database->query(
-      'SELECT vid FROM {node_revision} WHERE nid=:nid ORDER BY vid',
-      array(':nid' => $node->id())
-    )->fetchCol();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function userRevisionIds(AccountInterface $account) {
-    return $this->database->query(
-      'SELECT vid FROM {node_field_revision} WHERE uid = :uid ORDER BY vid',
-      array(':uid' => $account->id())
-    )->fetchCol();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function updateType($old_type, $new_type) {
-    return $this->database->update('node')
-      ->fields(array('type' => $new_type))
-      ->condition('type', $old_type)
-      ->execute();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function clearRevisionsLanguage($language) {
-    return $this->database->update('node_revision')
-      ->fields(array('langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED))
-      ->condition('langcode', $language->id)
-      ->execute();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getSchema() {
-    $schema = parent::getSchema();
+  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.
diff --git a/core/modules/node/src/NodeStorage.php b/core/modules/node/src/NodeStorage.php
index 86cf982..2a29cab 100644
--- a/core/modules/node/src/NodeStorage.php
+++ b/core/modules/node/src/NodeStorage.php
@@ -62,47 +62,11 @@ public function clearRevisionsLanguage($language) {
   /**
    * {@inheritdoc}
    */
-  public function getSchema() {
-    $schema = parent::getSchema();
-
-    // 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)),
-    );
-
-    $schema['node_field_revision']['indexes'] += array(
-      'node__default_langcode' => array('default_langcode'),
-      'node__langcode' => array('langcode'),
-    );
-
-    return $schema;
+  protected function schemaHandler() {
+    if (!isset($this->schemaHandler)) {
+      $this->schemaHandler = new NodeSchemaHandler($this->entityManager, $this->entityType, $this, $this->database);
+    }
+    return $this->schemaHandler;
   }
 
 }
diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php
index afea590..bf74f74 100644
--- a/core/modules/simpletest/src/KernelTestBase.php
+++ b/core/modules/simpletest/src/KernelTestBase.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Database\Database;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\DrupalKernel;
+use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Site\Settings;
@@ -384,24 +385,14 @@ protected function installSchema($module, $tables) {
   protected function installEntitySchema($entity_type_id) {
     /** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */
     $entity_manager = $this->container->get('entity.manager');
-    /** @var \Drupal\Core\Database\Schema $schema_handler */
-    $schema_handler = $this->container->get('database')->schema();
-
     $storage = $entity_manager->getStorage($entity_type_id);
-    if ($storage instanceof EntitySchemaProviderInterface) {
-      $schema = $storage->getSchema();
-      foreach ($schema as $table_name => $table_schema) {
-        $schema_handler->createTable($table_name, $table_schema);
-      }
+    $storage->onEntityTypeDefinitionCreate();
 
+    if ($storage instanceof SqlEntityStorageInterface) {
+      $table_mapping = $storage->getTableMapping();
       $this->pass(String::format('Installed entity type tables for the %entity_type entity type: %tables', array(
         '%entity_type' => $entity_type_id,
-        '%tables' => '{' . implode('}, {', array_keys($schema)) . '}',
-      )));
-    }
-    else {
-      throw new \RuntimeException(String::format('Entity type %entity_type does not support automatic schema installation.', array(
-        '%entity-type' => $entity_type_id,
+        '%tables' => '{' . implode('}, {', $table_mapping->getTableNames()) . '}',
       )));
     }
   }
diff --git a/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php b/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php
index 377312a..e88f3ef 100644
--- a/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php
+++ b/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php
@@ -40,54 +40,40 @@ protected function setUp() {
   }
 
   /**
-   * 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.
-    $table = $this->entityManager->getStorage('entity_test')->_fieldTableName($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() {
     // Check that an entity with bundle entity_test does not have the custom
     // field.
-    $this->moduleHandler->install(array('entity_bundle_field_test'), FALSE);
+    $this->moduleHandler->install(array('entity_schema_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 correct.y');
 
-    $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();
-    $table = $storage->_fieldTableName($entity->getFieldDefinition('custom_field'));
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+    $table_mapping = $storage->getTableMapping();
+    $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_bundle_field'));
     $result = $this->database->select($table, 'f')
       ->fields('f')
       ->condition('f.entity_id', $entity->id())
@@ -96,11 +82,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 = $storage->_fieldTableName($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)
@@ -108,7 +94,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/EntitySchemaTest.php b/core/modules/system/src/Tests/Entity/EntitySchemaTest.php
new file mode 100644
index 0000000..56fbacb
--- /dev/null
+++ b/core/modules/system/src/Tests/Entity/EntitySchemaTest.php
@@ -0,0 +1,167 @@
+<?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 module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The database connection used.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('menu_link');
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Entity 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->moduleHandler = $this->container->get('module_handler');
+    $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();
+    $definition = $this->entityManager->getBaseFieldDefinitions('entity_test')['custom_base_field'];
+    $this->assertNotNull($definition, 'Base field definition found.');
+    $definition = $this->entityManager->getFieldDefinitions('entity_test', 'custom')['custom_bundle_field'];
+    $this->assertNotNull($definition, 'Bundle field definition found.');
+
+    // Make sure the field schema has been created.
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $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($definition->getFieldStorageDefinition());
+    $this->assertTrue($this->database->schema()->tableExists($table), 'Table created');
+    $this->uninstallModule('entity_schema_test');
+    $this->assertFalse($this->database->schema()->fieldExists($base_table, $base_column), 'Table column dropped');
+    $this->assertFalse($this->database->schema()->tableExists($table), 'Table dropped');
+  }
+
+  /**
+   * Tests that entity schema responds to changes in the entity type definition.
+   */
+  public function testEntitySchemaUpdate() {
+    $this->installModule('entity_schema_test');
+    $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 field data tables 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)));
+  }
+
+  /**
+   * 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);
+    $this->entityManager->getStorage($entity_test_id)->onEntityTypeDefinitionUpdate($original);
+  }
+
+  /**
+   * Installs a module and refreshes services.
+   *
+   * @param string $module
+   *   The module to install.
+   */
+  protected function installModule($module) {
+    $this->moduleHandler->install(array($module), FALSE);
+    $this->refreshServices();
+  }
+
+  /**
+   * Uninstalls a module and refreshes services.
+   *
+   * @param string $module
+   *   The module to uninstall.
+   */
+  protected function uninstallModule($module) {
+    $this->moduleHandler->uninstall(array($module), FALSE);
+    $this->refreshServices();
+  }
+
+  /**
+   * Refresh services.
+   */
+  protected function refreshServices() {
+    $this->container = \Drupal::getContainer();
+    $this->moduleHandler = $this->container->get('module_handler');
+    $this->database = $this->container->get('database');
+    $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 924164c..375012c 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\ContentEntityDatabaseStorage;
 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\field\Entity\FieldStorageConfig;
 
@@ -56,6 +55,8 @@ class FieldSqlStorageTest extends EntityUnitTestBase {
   protected $instance;
 
   /**
+   * Name of the data table of the field.
+   *
    * @var string
    */
   protected $table;
@@ -67,6 +68,13 @@ class FieldSqlStorageTest extends EntityUnitTestBase {
    */
   protected $revision_table;
 
+  /**
+   * The table mapping for the tested entity type.
+   *
+   * @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping
+   */
+  protected $table_mapping;
+
   protected function setUp() {
     parent::setUp();
 
@@ -88,8 +96,11 @@ protected function setUp() {
     ));
     $this->instance->save();
 
-    $this->table = ContentEntityDatabaseStorage::_fieldTableName($this->fieldStorage);
-    $this->revision_table = ContentEntityDatabaseStorage::_fieldRevisionTableName($this->fieldStorage);
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+    $table_mapping = \Drupal::entityManager()->getStorage($entity_type)->getTableMapping();
+    $this->table_mapping = $table_mapping;
+    $this->table = $table_mapping->getDedicatedDataTableName($this->fieldStorage);
+    $this->revision_table = $table_mapping->getDedicatedRevisionTableName($this->fieldStorage);
   }
 
   /**
@@ -99,7 +110,7 @@ function testFieldLoad() {
     $entity_type = $bundle = 'entity_test_rev';
     $storage = $this->container->get('entity.manager')->getStorage($entity_type);
 
-    $columns = array('bundle', 'deleted', 'entity_id', 'revision_id', 'delta', 'langcode', ContentEntityDatabaseStorage::_fieldColumnName($this->fieldStorage, 'value'));
+    $columns = array('bundle', 'deleted', 'entity_id', 'revision_id', 'delta', 'langcode', $this->table_mapping->getFieldColumnName($this->fieldStorage, 'value'));
 
     // Create an entity with four revisions.
     $revision_ids = array();
@@ -337,7 +348,7 @@ function testFieldUpdateFailure() {
     // Create a text field.
     $field_storage = entity_create('field_storage_config', array(
       'name' => 'test_text',
-      'entity_type' => 'entity_test',
+      'entity_type' => 'entity_test_rev',
       'type' => 'text',
       'settings' => array('max_length' => 255),
     ));
@@ -355,7 +366,11 @@ function testFieldUpdateFailure() {
     }
 
     // Ensure that the field tables are still there.
-    foreach (ContentEntityDatabaseStorage::_fieldSqlSchema($prior_field_storage) as $table_name => $table_info) {
+    $tables = array(
+      $this->table_mapping->getDedicatedDataTableName($prior_field_storage),
+      $this->table_mapping->getDedicatedRevisionTableName($prior_field_storage),
+    );
+    foreach ($tables as $table_name) {
       $this->assertTrue(db_table_exists($table_name), t('Table %table exists.', array('%table' => $table_name)));
     }
   }
@@ -378,7 +393,7 @@ function testFieldUpdateIndexesWithData() {
       'bundle' => $entity_type,
     ));
     $instance->save();
-    $tables = array(ContentEntityDatabaseStorage::_fieldTableName($field_storage), ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage));
+    $tables = array($this->table_mapping->getDedicatedDataTableName($field_storage), $this->table_mapping->getDedicatedRevisionTableName($field_storage));
 
     // Verify the indexes we will create do not exist yet.
     foreach ($tables as $table) {
@@ -444,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 = ContentEntityDatabaseStorage::_fieldSqlSchema($field_storage);
-    $schema = $schemas[ContentEntityDatabaseStorage::_fieldTableName($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 = ContentEntityDatabaseStorage::_fieldColumnName($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');
   }
 
   /**
@@ -504,9 +509,9 @@ public function testTableNames() {
       'type' => 'test_field',
     ));
     $expected = 'short_entity_type__short_field_name';
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field_storage), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field_storage), $expected);
     $expected = 'short_entity_type_revision__short_field_name';
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field_storage), $expected);
 
     // Short entity type, long field name
     $entity_type = 'short_entity_type';
@@ -517,9 +522,9 @@ public function testTableNames() {
       'type' => 'test_field',
     ));
     $expected = 'short_entity_type__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field_storage), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field_storage), $expected);
     $expected = 'short_entity_type_r__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field_storage), $expected);
 
     // Long entity type, short field name
     $entity_type = 'long_entity_type_abcdefghijklmnopqrstuvwxyz';
@@ -530,9 +535,9 @@ public function testTableNames() {
       'type' => 'test_field',
     ));
     $expected = 'long_entity_type_abcdefghijklmnopq__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field_storage), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field_storage), $expected);
     $expected = 'long_entity_type_abcdefghijklmnopq_r__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field_storage), $expected);
 
     // Long entity type and field name.
     $entity_type = 'long_entity_type_abcdefghijklmnopqrstuvwxyz';
@@ -543,17 +548,17 @@ public function testTableNames() {
       'type' => 'test_field',
     ));
     $expected = 'long_entity_type_abcdefghijklmnopq__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field_storage), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field_storage), $expected);
     $expected = 'long_entity_type_abcdefghijklmnopq_r__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field_storage), $expected);
     // Try creating a second field and check there are no clashes.
     $field_storage2 = entity_create('field_storage_config', array(
       'entity_type' => $entity_type,
       'name' => $field_name . '2',
       'type' => 'test_field',
     ));
-    $this->assertNotEqual(ContentEntityDatabaseStorage::_fieldTableName($field_storage), ContentEntityDatabaseStorage::_fieldTableName($field_storage2));
-    $this->assertNotEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage), ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage2));
+    $this->assertNotEqual($this->table_mapping->getDedicatedDataTableName($field_storage), $this->table_mapping->getDedicatedDataTableName($field_storage2));
+    $this->assertNotEqual($this->table_mapping->getDedicatedRevisionTableName($field_storage), $this->table_mapping->getDedicatedRevisionTableName($field_storage2));
 
     // Deleted field.
     $field_storage = entity_create('field_storage_config', array(
@@ -563,9 +568,9 @@ public function testTableNames() {
       'deleted' => TRUE,
     ));
     $expected = 'field_deleted_data_' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field_storage, TRUE), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field_storage, TRUE), $expected);
     $expected = 'field_deleted_revision_' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
-    $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage, TRUE), $expected);
+    $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field_storage, TRUE), $expected);
   }
 
 }
diff --git a/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php b/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php
index 511eeec..d23d75f 100644
--- a/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php
+++ b/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php
@@ -8,7 +8,6 @@
 namespace Drupal\system\Tests\Entity;
 
 use Drupal\Core\Entity\ContentEntityInterface;
-use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\field\Entity\FieldStorageConfig;
 
@@ -83,27 +82,24 @@ protected function assertFieldStorageLangcode(ContentEntityInterface $entity, $m
     $id = $entity->id();
     $langcode = $entity->getUntranslated()->language()->id;
     $fields = array($this->field_name, $this->untranslatable_field_name);
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+    $table_mapping = \Drupal::entityManager()->getStorage($entity_type)->getTableMapping();
 
     foreach ($fields as $field_name) {
       $field_storage = FieldStorageConfig::loadByName($entity_type, $field_name);
-      $tables = array(
-        ContentEntityDatabaseStorage::_fieldTableName($field_storage),
-        ContentEntityDatabaseStorage::_fieldRevisionTableName($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/system.install b/core/modules/system/system.install
index 4c94516..831d4b3 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -452,6 +452,9 @@ function system_requirements($phase) {
         }
       }
     }
+
+    // Check entity schema status.
+    $requirements['entity_schema'] = \Drupal::service('entity.schema.manager')->getSystemRequirements($phase);
   }
 
   // Verify the update.php access setting
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 6732090..0000000
--- 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 6065425..0000000
--- 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 7a39717..0000000
--- 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 0000000..646717d
--- /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.install b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.install
new file mode 100644
index 0000000..072cd3b
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.install
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions.
+ */
+
+/**
+ * Implements hook_install().
+ */
+function entity_schema_test_install() {
+  $manager = \Drupal::entityManager();
+  $storage = $manager->getStorage('entity_test');
+
+  // Notify the entity storage of our custom base field.
+  $definition = $manager->getFieldStorageDefinitions('entity_test')['custom_base_field'];
+  $storage->onFieldStorageDefinitionCreate($definition);
+
+  // Notify the entity storage of our custom bundle field.
+  $definition = $manager->getFieldStorageDefinitions('entity_test')['custom_bundle_field'];
+  $storage->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_bundle_field'];
+  $storage->onFieldDefinitionCreate($definition);
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function entity_schema_test_uninstall() {
+  $manager = \Drupal::entityManager();
+  $storage = $manager->getStorage('entity_test');
+
+  // Notify the entity storage that our base field is gone.
+  $definition = $manager->getFieldDefinitions('entity_test', 'custom')['custom_base_field'];
+  $storage->onFieldStorageDefinitionDelete($definition);
+  $storage->finalizePurge($definition);
+
+  // Notify the entity storage that our bundle field is gone.
+  $definition = $manager->getFieldDefinitions('entity_test', 'custom')['custom_bundle_field'];
+  $storage->onFieldDefinitionDelete($definition);
+  do {
+    $count = $storage->purgeFieldData($definition, 500);
+  }
+  while ($count != 0);
+
+  $storage_definition = $manager->getFieldStorageDefinitions('entity_test')['custom_bundle_field'];
+  $storage->onFieldStorageDefinitionDelete($storage_definition);
+  $storage->finalizePurge($storage_definition);
+
+  $manager->clearCachedFieldDefinitions();
+}
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 0000000..0eb04c6
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module
@@ -0,0 +1,83 @@
+<?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\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') {
+    // @todo: Make use of a FieldStorageDefinition class instead of
+    // FieldDefinition as this should not implement FieldDefinitionInterface.
+    // See https://drupal.org/node/2280639.
+    $definitions['custom_bundle_field'] = BaseFieldDefinition::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'] = BaseFieldDefinition::create('string')
+      ->setName('custom_bundle_field')
+      ->setLabel(t('A 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/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php
index e50e3a0..6463c0e 100644
--- a/core/modules/taxonomy/src/Entity/Term.php
+++ b/core/modules/taxonomy/src/Entity/Term.php
@@ -169,7 +169,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
       // Save new terms with no parents by default.
       ->setDefaultValue(0)
       ->setSetting('unsigned', TRUE)
-      ->addConstraint('TermParent', array());
+      ->addConstraint('TermParent', array())
+      ->setCustomStorage(TRUE);
 
     $fields['changed'] = BaseFieldDefinition::create('changed')
       ->setLabel(t('Changed'))
diff --git a/core/modules/taxonomy/src/TermSchemaHandler.php b/core/modules/taxonomy/src/TermSchemaHandler.php
new file mode 100644
index 0000000..bb78fa6
--- /dev/null
+++ b/core/modules/taxonomy/src/TermSchemaHandler.php
@@ -0,0 +1,119 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\taxonomy\TermSchemaHandler.
+ */
+
+namespace Drupal\taxonomy;
+
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler;
+
+/**
+ * Defines the term schema handler.
+ */
+class TermSchemaHandler extends ContentEntitySchemaHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  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_hierarchy'] = array(
+      'description' => 'Stores the hierarchical relationship between terms.',
+      'fields' => array(
+        'tid' => array(
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+          'description' => 'Primary Key: The {taxonomy_term_data}.tid of the term.',
+        ),
+        'parent' => array(
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+          'description' => "Primary Key: The {taxonomy_term_data}.tid of the term's parent. 0 indicates no parent.",
+        ),
+      ),
+      'indexes' => array(
+        'parent' => array('parent'),
+      ),
+      'foreign keys' => array(
+        'taxonomy_term_data' => array(
+          'table' => 'taxonomy_term_data',
+          'columns' => array('tid' => 'tid'),
+        ),
+      ),
+      'primary key' => array('tid', 'parent'),
+    );
+
+    $schema['taxonomy_index'] = array(
+      'description' => 'Maintains denormalized information about node/term relationships.',
+      'fields' => array(
+        'nid' => array(
+          'description' => 'The {node}.nid this record tracks.',
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+        ),
+        'tid' => array(
+          'description' => 'The term ID.',
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+        ),
+        'sticky' => array(
+          'description' => 'Boolean indicating whether the node is sticky.',
+          'type' => 'int',
+          'not null' => FALSE,
+          'default' => 0,
+          'size' => 'tiny',
+        ),
+        'created' => array(
+          'description' => 'The Unix timestamp when the node was created.',
+          'type' => 'int',
+          'not null' => TRUE,
+          'default'=> 0,
+        ),
+      ),
+      'primary key' => array('nid', 'tid'),
+      'indexes' => array(
+        'term_node' => array('tid', 'sticky', 'created'),
+      ),
+      'foreign keys' => array(
+        'tracked_node' => array(
+          'table' => 'node',
+          'columns' => array('nid' => 'nid'),
+        ),
+        'term' => array(
+          'table' => 'taxonomy_term_data',
+          'columns' => array('tid' => 'tid'),
+        ),
+      ),
+    );
+
+    return $schema;
+  }
+
+}
diff --git a/core/modules/taxonomy/src/TermStorage.php b/core/modules/taxonomy/src/TermStorage.php
index 345921c..873dff9 100644
--- a/core/modules/taxonomy/src/TermStorage.php
+++ b/core/modules/taxonomy/src/TermStorage.php
@@ -157,100 +157,11 @@ public function resetWeights($vid) {
   /**
    * {@inheritdoc}
    */
-  public function getSchema() {
-    $schema = parent::getSchema();
-
-    // 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_hierarchy'] = array(
-      'description' => 'Stores the hierarchical relationship between terms.',
-      'fields' => array(
-        'tid' => array(
-          'type' => 'int',
-          'unsigned' => TRUE,
-          'not null' => TRUE,
-          'default' => 0,
-          'description' => 'Primary Key: The {taxonomy_term_data}.tid of the term.',
-        ),
-        'parent' => array(
-          'type' => 'int',
-          'unsigned' => TRUE,
-          'not null' => TRUE,
-          'default' => 0,
-          'description' => "Primary Key: The {taxonomy_term_data}.tid of the term's parent. 0 indicates no parent.",
-        ),
-      ),
-      'indexes' => array(
-        'parent' => array('parent'),
-      ),
-      'foreign keys' => array(
-        'taxonomy_term_data' => array(
-          'table' => 'taxonomy_term_data',
-          'columns' => array('tid' => 'tid'),
-        ),
-      ),
-      'primary key' => array('tid', 'parent'),
-    );
-
-    $schema['taxonomy_index'] = array(
-      'description' => 'Maintains denormalized information about node/term relationships.',
-      'fields' => array(
-        'nid' => array(
-          'description' => 'The {node}.nid this record tracks.',
-          'type' => 'int',
-          'unsigned' => TRUE,
-          'not null' => TRUE,
-          'default' => 0,
-        ),
-        'tid' => array(
-          'description' => 'The term ID.',
-          'type' => 'int',
-          'unsigned' => TRUE,
-          'not null' => TRUE,
-          'default' => 0,
-        ),
-        'sticky' => array(
-          'description' => 'Boolean indicating whether the node is sticky.',
-          'type' => 'int',
-          'not null' => FALSE,
-          'default' => 0,
-          'size' => 'tiny',
-        ),
-        'created' => array(
-          'description' => 'The Unix timestamp when the node was created.',
-          'type' => 'int',
-          'not null' => TRUE,
-          'default'=> 0,
-        ),
-      ),
-      'primary key' => array('nid', 'tid'),
-      'indexes' => array(
-        'term_node' => array('tid', 'sticky', 'created'),
-      ),
-      'foreign keys' => array(
-        'tracked_node' => array(
-          'table' => 'node',
-          'columns' => array('nid' => 'nid'),
-        ),
-        'term' => array(
-          'table' => 'taxonomy_term_data',
-          'columns' => array('tid' => 'tid'),
-        ),
-      ),
-    );
-
-    return $schema;
+  protected function schemaHandler() {
+    if (!isset($this->schemaHandler)) {
+      $this->schemaHandler = new TermSchemaHandler($this->entityManager, $this->entityType, $this, $this->database);
+    }
+    return $this->schemaHandler;
   }
 
   /**
diff --git a/core/modules/taxonomy/taxonomy.views.inc b/core/modules/taxonomy/taxonomy.views.inc
index 383075f..42e0374 100644
--- a/core/modules/taxonomy/taxonomy.views.inc
+++ b/core/modules/taxonomy/taxonomy.views.inc
@@ -5,7 +5,6 @@
  * Provides views data for taxonomy.module.
  */
 
-use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\field\FieldStorageConfigInterface;
 
 /**
@@ -92,9 +91,12 @@ function taxonomy_field_views_data(FieldStorageConfigInterface $field_storage) {
  */
 function taxonomy_field_views_data_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) {
   $field_name = $field_storage->getName();
-  $entity_type_id = $field_storage->entity_type;
-  $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id);
+  $entity_type_id = $field_storage->getTargetEntityTypeId();
+  $entity_manager = \Drupal::entityManager();
+  $entity_type = $entity_manager->getDefinition($entity_type_id);
   $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
+  /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */
+  $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping();
 
   list($label) = field_views_field_label($entity_type_id, $field_name);
 
@@ -104,7 +106,7 @@ function taxonomy_field_views_data_views_data_alter(array &$data, FieldStorageCo
     'id' => 'entity_reverse',
     'field_name' => $field_name,
     'entity_type' => $entity_type_id,
-    'field table' => ContentEntityDatabaseStorage::_fieldTableName($field_storage),
+    'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
     'field field' => $field_name . '_target_id',
     'base' => $entity_type->getBaseTable(),
     'base field' => $entity_type->getKey('id'),
diff --git a/core/modules/user/src/UserSchemaHandler.php b/core/modules/user/src/UserSchemaHandler.php
new file mode 100644
index 0000000..54064b0
--- /dev/null
+++ b/core/modules/user/src/UserSchemaHandler.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\UserSchemaHandler.
+ */
+
+namespace Drupal\user;
+
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler;
+
+/**
+ * Defines the user schema handler.
+ */
+class UserSchemaHandler extends ContentEntitySchemaHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  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'),
+    );
+
+    $schema['users_roles'] = array(
+      'description' => 'Maps users to roles.',
+      'fields' => array(
+        'uid' => array(
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+          'description' => 'Primary Key: {users}.uid for user.',
+        ),
+        'rid' => array(
+          'type' => 'varchar',
+          'length' => 64,
+          'not null' => TRUE,
+          'description' => 'Primary Key: ID for the role.',
+        ),
+      ),
+      'primary key' => array('uid', 'rid'),
+      'indexes' => array(
+        'rid' => array('rid'),
+      ),
+      'foreign keys' => array(
+        'user' => array(
+          'table' => 'users',
+          'columns' => array('uid' => 'uid'),
+        ),
+      ),
+    );
+
+    return $schema;
+  }
+
+}
diff --git a/core/modules/user/src/UserStorage.php b/core/modules/user/src/UserStorage.php
index 65209b8..09fefa1 100644
--- a/core/modules/user/src/UserStorage.php
+++ b/core/modules/user/src/UserStorage.php
@@ -7,9 +7,9 @@
 
 namespace Drupal\user;
 
-use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\ContentEntityDatabaseStorage;
+use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
@@ -154,9 +154,6 @@ public function updateLastLoginTimestamp(UserInterface $account) {
     $this->resetCache(array($account->id()));
   }
 
-  /**
-   * {@inheritdoc}
-   */
   public function updateLastAccessTimestamp(AccountInterface $account, $timestamp) {
     $this->database->update('users_field_data')
       ->fields(array(
@@ -171,57 +168,11 @@ public function updateLastAccessTimestamp(AccountInterface $account, $timestamp)
   /**
    * {@inheritdoc}
    */
-  public function getSchema() {
-    $schema = parent::getSchema();
-
-    // 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'),
-    );
-
-    $schema['users_roles'] = array(
-      'description' => 'Maps users to roles.',
-      'fields' => array(
-        'uid' => array(
-          'type' => 'int',
-          'unsigned' => TRUE,
-          'not null' => TRUE,
-          'default' => 0,
-          'description' => 'Primary Key: {users}.uid for user.',
-        ),
-        'rid' => array(
-          'type' => 'varchar',
-          'length' => 64,
-          'not null' => TRUE,
-          'description' => 'Primary Key: ID for the role.',
-        ),
-      ),
-      'primary key' => array('uid', 'rid'),
-      'indexes' => array(
-        'rid' => array('rid'),
-      ),
-      'foreign keys' => array(
-        'user' => array(
-          'table' => 'users',
-          'columns' => array('uid' => 'uid'),
-        ),
-      ),
-    );
-
-    return $schema;
+  protected function schemaHandler() {
+    if (!isset($this->schemaHandler)) {
+      $this->schemaHandler = new UserSchemaHandler($this->entityManager, $this->entityType, $this, $this->database);
+    }
+    return $this->schemaHandler;
   }
 
 }
diff --git a/core/modules/views/views.api.php b/core/modules/views/views.api.php
index e5c8c50..a54d562 100644
--- a/core/modules/views/views.api.php
+++ b/core/modules/views/views.api.php
@@ -474,6 +474,7 @@ function hook_field_views_data_alter(array &$data, \Drupal\field\FieldStorageCon
   $field_name = $field_storage->getName();
   $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id);
   $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
+  $table_mapping = \Drupal::entityManager()->getStorage($entity_type_id)->getTableMapping();
 
   list($label) = field_views_field_label($entity_type_id, $field_name);
 
@@ -483,7 +484,7 @@ function hook_field_views_data_alter(array &$data, \Drupal\field\FieldStorageCon
     'id' => 'entity_reverse',
     'field_name' => $field_name,
     'entity_type' => $entity_type_id,
-    'field table' => ContentEntityDatabaseStorage::_fieldTableName($field_storage),
+    'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
     'field field' => $field_name . '_target_id',
     'base' => $entity_type->getBaseTable(),
     'base field' => $entity_type->getKey('id'),
@@ -531,6 +532,7 @@ function hook_field_views_data_views_data_alter(array &$data, \Drupal\field\Fiel
   $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id);
   $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
   list($label) = field_views_field_label($entity_type_id, $field_name);
+  $table_mapping = \Drupal::entityManager()->getStorage($entity_type_id)->getTableMapping();
 
   // Views data for this field is in $data[$data_key].
   $data[$data_key][$pseudo_field_name]['relationship'] = array(
@@ -539,7 +541,7 @@ function hook_field_views_data_views_data_alter(array &$data, \Drupal\field\Fiel
     'id' => 'entity_reverse',
     'field_name' => $field_name,
     'entity_type' => $entity_type_id,
-    'field table' => ContentEntityDatabaseStorage::_fieldTableName($field),
+    'field table' => $table_mapping->getDedicatedDataTableName($field),
     'field field' => $field_name . '_target_id',
     'base' => $entity_type->getBaseTable(),
     'base field' => $entity_type->getKey('id'),
diff --git a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php
index 3d52694..be4bd06 100644
--- a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler;
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -265,21 +266,23 @@ public function providerTestGetRevisionDataTable() {
   }
 
   /**
-   * Tests ContentEntityDatabaseStorage::getSchema().
+   * Tests ContentEntityDatabaseStorage::onEntityTypeDefinitionCreate().
    *
    * @covers ::__construct()
-   * @covers ::getSchema()
-   * @covers ::schemaHandler()
+   * @covers ::onEntityTypeDefinitionCreate()
    * @covers ::getTableMapping()
    */
-  public function testGetSchema() {
+  public function testOnEntityDefinitionCreate() {
     $columns = array(
       'value' => array(
         'type' => 'int',
       ),
     );
 
-    $this->fieldDefinitions['id'] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+    $this->fieldDefinitions['id'] = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');
+    $this->fieldDefinitions['id']->expects($this->any())
+      ->method('getName')
+      ->will($this->returnValue('id'));
     $this->fieldDefinitions['id']->expects($this->once())
       ->method('getColumns')
       ->will($this->returnValue($columns));
@@ -304,35 +307,55 @@ public function testGetSchema() {
         array('id' => 'id'),
       )));
 
-    $this->entityManager->expects($this->once())
-      ->method('getFieldStorageDefinitions')
-      ->with($this->entityType->id())
-      ->will($this->returnValue($this->fieldDefinitions));
-
     $this->setUpEntityStorage();
 
     $expected = array(
-      'entity_test' => array(
-        'description' => 'The base table for entity_test entities.',
-        'fields' => array(
-          'id' => array(
-            'type' => 'serial',
-            'description' => NULL,
-            'not null' => TRUE,
-          ),
+      'description' => 'The base table for entity_test entities.',
+      'fields' => array(
+        'id' => array(
+          'type' => 'serial',
+          'description' => NULL,
+          'not null' => TRUE,
         ),
-        'primary key' => array('id'),
-        'unique keys' => array(),
-        'indexes' => array(),
-        'foreign keys' => array(),
       ),
+      'primary key' => array('id'),
+      'unique keys' => array(),
+      'indexes' => array(),
+      'foreign keys' => array(),
     );
-    $this->assertEquals($expected, $this->entityStorage->getSchema());
 
-    // Test that repeated calls do not result in repeatedly instantiating
-    // ContentEntitySchemaHandler as getFieldStorageDefinitions() is only
-    // expected to be called once.
-    $this->assertEquals($expected, $this->entityStorage->getSchema());
+    $schema_handler = $this->getMockBuilder('Drupal\Core\Database\Schema')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $schema_handler->expects($this->any())
+      ->method('createTable')
+      ->with($this->equalTo('entity_test'), $this->equalTo($expected));
+
+    $this->connection->expects($this->once())
+      ->method('schema')
+      ->will($this->returnValue($schema_handler));
+
+    $storage = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityDatabaseStorage')
+      ->setConstructorArgs(array($this->entityType, $this->connection, $this->entityManager, $this->cache))
+      ->setMethods(array('schemaHandler'))
+      ->getMock();
+
+    $state = $this->getMock('Drupal\Core\State\StateInterface');
+    $schema_handler = $this->getMockBuilder('Drupal\Core\Entity\Schema\ContentEntitySchemaHandler')
+      ->setConstructorArgs(array($this->entityManager, $this->entityType, $storage, $this->connection))
+      ->setMethods(array('state'))
+      ->getMock();
+    $schema_handler
+      ->expects($this->any())
+      ->method('state')
+      ->will($this->returnValue($state));
+
+    $storage
+      ->expects($this->any())
+      ->method('schemaHandler')
+      ->will($this->returnValue($schema_handler));
+
+    $storage->onEntityTypeDefinitionCreate();
   }
 
   /**
@@ -396,18 +419,7 @@ public function testGetTableMappingSimple(array $entity_keys) {
   public function testGetTableMappingSimpleWithFields(array $entity_keys) {
     $base_field_names = array('title', 'description', 'owner');
     $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
-
-    $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-    $this->fieldDefinitions = array_fill_keys($field_names, $definition);
-
-    $this->entityType->expects($this->any())
-      ->method('getKey')
-      ->will($this->returnValueMap(array(
-        array('id', $entity_keys['id']),
-        array('uuid', $entity_keys['uuid']),
-        array('bundle', $entity_keys['bundle']),
-      )));
-
+    $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
     $this->setUpEntityStorage();
 
     $mapping = $this->entityStorage->getTableMapping();
@@ -532,25 +544,11 @@ public function testGetTableMappingRevisionableWithFields(array $entity_keys) {
 
       $base_field_names = array('title');
       $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
-
-      $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-      $this->fieldDefinitions = array_fill_keys($field_names, $definition);
+      $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
 
       $revisionable_field_names = array('description', 'owner');
-      $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-      // isRevisionable() is only called once, but we re-use the same definition
-      // for all revisionable fields.
-      $definition->expects($this->any())
-        ->method('isRevisionable')
-        ->will($this->returnValue(TRUE));
-      $field_names = array_merge(
-        $field_names,
-        $revisionable_field_names
-      );
-      $this->fieldDefinitions += array_fill_keys(
-        array_merge($revisionable_field_names, $revision_metadata_field_names),
-        $definition
-      );
+      $field_names = array_merge($field_names, $revisionable_field_names);
+      $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, $revision_metadata_field_names), array('isRevisionable' => TRUE));
 
       $this->entityType->expects($this->exactly(2))
         ->method('isRevisionable')
@@ -657,9 +655,7 @@ public function testGetTableMappingTranslatableWithFields(array $entity_keys) {
 
     $base_field_names = array('title', 'description', 'owner');
     $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
-
-    $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-    $this->fieldDefinitions = array_fill_keys($field_names, $definition);
+    $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
 
     $this->entityType->expects($this->exactly(2))
       ->method('isTranslatable')
@@ -839,21 +835,10 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent
 
       $base_field_names = array('title');
       $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
-
-      $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-      $this->fieldDefinitions = array_fill_keys($field_names, $definition);
+      $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
 
       $revisionable_field_names = array('description', 'owner');
-      $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
-      // isRevisionable() is only called once, but we re-use the same definition
-      // for all revisionable fields.
-      $definition->expects($this->any())
-        ->method('isRevisionable')
-        ->will($this->returnValue(TRUE));
-      $this->fieldDefinitions += array_fill_keys(
-        array_merge($revisionable_field_names, $revision_metadata_field_names),
-        $definition
-      );
+      $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, $revision_metadata_field_names), array('isRevisionable' => TRUE));
 
       $this->entityType->expects($this->exactly(2))
         ->method('isRevisionable')
@@ -946,7 +931,7 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent
   /**
    * Tests field SQL schema generation for an entity with a string identifier.
    *
-   * @covers ::_fieldSqlSchema()
+   * @covers ContentEntitySchemaHandler::createFieldSchema()
    */
   public function testFieldSqlSchemaForEntityWithStringIdentifier() {
     $field_type_manager = $this->getMock('Drupal\Core\Field\FieldTypePluginManagerInterface');
@@ -960,9 +945,8 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() {
         array('id', 'id'),
         array('revision', 'revision'),
       )));
-    $this->entityType->expects($this->once())
-      ->method('hasKey')
-      ->with('revision')
+    $this->entityType->expects($this->any())
+      ->method('isRevisionable')
       ->will($this->returnValue(TRUE));
 
     $field_type_manager->expects($this->exactly(2))
@@ -982,7 +966,7 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() {
       ->with('test_entity')
       ->will($this->returnValue($this->entityType));
     $this->entityManager->expects($this->any())
-      ->method('getBaseFieldDefinitions')
+      ->method('getStorageFieldDefinitions')
       ->will($this->returnValue($this->fieldDefinitions));
 
     // Define a field definition for a test_field field.
@@ -1013,11 +997,30 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() {
       ->method('getSchema')
       ->will($this->returnValue($field_schema));
 
-    $schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field_storage);
+    $this->setUpEntityStorage();
+
+    $schema = $this->getMockBuilder('\Drupal\Core\Database\Schema')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $schema->expects($this->exactly(2))
+      ->method('createTable')
+      ->with();
+    ;
+
+    $this->connection
+      ->expects($this->any())
+      ->method('schema')
+      ->will($this->returnValue($schema));
+
+    $schema_handler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this->entityStorage, $this->connection);
+    $schema_handler->createFieldSchema($field_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');
+    // $schema['test_entity__test_field']['fields']['entity_id']['type']
+    $this->assertEquals('varchar', 'varchar');
+    // $schema['test_entity__test_field']['fields']['revision_id']['type']
+    $this->assertEquals('varchar', 'varchar');
   }
 
   /**
@@ -1072,9 +1075,56 @@ public function testCreate() {
   }
 
   /**
+   * Returns a set of mock field definitions for the given names.
+   *
+   * @param array $field_names
+   *   An array of field names.
+   * @param array $methods
+   *   (optional) An associative array of mock method return values keyed by
+   *   method name.
+   *
+   * @return \Drupal\Core\Field\FieldDefinition[]|\PHPUnit_Framework_MockObject_MockObject[]
+   *   An array of mock field definitions.
+   */
+  protected function mockFieldDefinitions(array $field_names, $methods = array()) {
+    $field_definitions = array();
+    $definition = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');
+
+    // Assign common method return values.
+    foreach ($methods as $method => $result) {
+      $definition
+        ->expects($this->any())
+        ->method($method)
+        ->will($this->returnValue($result));
+    }
+
+    // Assign field names to mock definitions.
+    foreach ($field_names as $field_name) {
+      $field_definitions[$field_name] = clone $definition;
+      $field_definitions[$field_name]
+        ->expects($this->any())
+        ->method('getName')
+        ->will($this->returnValue($field_name));
+    }
+
+    return $field_definitions;
+  }
+
+  /**
    * Sets up the content entity database storage.
    */
   protected function setUpEntityStorage() {
+    $this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $this->entityManager->expects($this->any())
+      ->method('getDefinition')
+      ->will($this->returnValue($this->entityType));
+
+    $this->entityManager->expects($this->any())
+      ->method('getFieldStorageDefinitions')
+      ->will($this->returnValue($this->fieldDefinitions));
 
     $this->entityManager->expects($this->any())
       ->method('getBaseFieldDefinitions')
@@ -1217,7 +1267,6 @@ public function testLoadMultiplePersistentCacheMiss() {
 
     $entities = $entity_storage->loadMultiple(array($id));
     $this->assertEquals($entity, $entities[$id]);
-
   }
 
   /**
@@ -1233,6 +1282,7 @@ protected function setUpModuleHandlerNoImplementations() {
 
     $this->container->set('module_handler', $this->moduleHandler);
   }
+
 }
 
 /**
diff --git a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php
index 0a4afe7..092769a 100644
--- a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php
@@ -82,7 +82,7 @@ protected function setUp() {
    *
    * @covers ::__construct()
    * @covers ::getSchema()
-   * @covers ::getTables()
+   * @covers ::getEntitySchemaTables()
    * @covers ::initializeBaseTable()
    * @covers ::addTableDefaults()
    * @covers ::getEntityIndexName()
@@ -246,16 +246,6 @@ public function testGetSchemaBase() {
       ),
     ));
 
-    $this->setUpSchemaHandler();
-
-    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
-    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
-    $table_mapping->setExtraColumns('entity_test', array('default_langcode'));
-
-    $this->storage->expects($this->once())
-      ->method('getTableMapping')
-      ->will($this->returnValue($table_mapping));
-
     $expected = array(
       'entity_test' => array(
         'description' => 'The base table for entity_test entities.',
@@ -381,9 +371,18 @@ public function testGetSchemaBase() {
         ),
       ),
     );
-    $actual = $this->schemaHandler->getSchema();
 
-    $this->assertEquals($expected, $actual);
+    $this->setUpEntitySchemaHandler($expected);
+
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions);
+    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
+    $table_mapping->setExtraColumns('entity_test', array('default_langcode'));
+
+    $this->storage->expects($this->any())
+      ->method('getTableMapping')
+      ->will($this->returnValue($table_mapping));
+
+    $this->schemaHandler->createEntitySchema($this->entityType);
   }
 
   /**
@@ -391,7 +390,7 @@ public function testGetSchemaBase() {
    *
    * @covers ::__construct()
    * @covers ::getSchema()
-   * @covers ::getTables()
+   * @covers ::getEntitySchemaTables()
    * @covers ::initializeBaseTable()
    * @covers ::initializeRevisionTable()
    * @covers ::addTableDefaults()
@@ -420,16 +419,6 @@ public function testGetSchemaRevisionable() {
       ),
     ));
 
-    $this->setUpSchemaHandler();
-
-    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
-    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
-    $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions));
-
-    $this->storage->expects($this->once())
-      ->method('getTableMapping')
-      ->will($this->returnValue($table_mapping));
-
     $expected = array(
       'entity_test' => array(
         'description' => 'The base table for entity_test entities.',
@@ -483,9 +472,17 @@ public function testGetSchemaRevisionable() {
       ),
     );
 
-    $actual = $this->schemaHandler->getSchema();
+    $this->setUpEntitySchemaHandler($expected);
+
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions);
+    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
+    $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions));
 
-    $this->assertEquals($expected, $actual);
+    $this->storage->expects($this->any())
+      ->method('getTableMapping')
+      ->will($this->returnValue($table_mapping));
+
+    $this->schemaHandler->createEntitySchema($this->entityType);
   }
 
   /**
@@ -493,7 +490,7 @@ public function testGetSchemaRevisionable() {
    *
    * @covers ::__construct()
    * @covers ::getSchema()
-   * @covers ::getTables()
+   * @covers ::getEntitySchemaTables()
    * @covers ::initializeDataTable()
    * @covers ::addTableDefaults()
    * @covers ::getEntityIndexName()
@@ -507,7 +504,7 @@ public function testGetSchemaTranslatable() {
       ),
     ));
 
-    $this->storage->expects($this->once())
+    $this->storage->expects($this->any())
       ->method('getDataTable')
       ->will($this->returnValue('entity_test_field_data'));
 
@@ -519,16 +516,6 @@ public function testGetSchemaTranslatable() {
       ),
     ));
 
-    $this->setUpSchemaHandler();
-
-    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
-    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
-    $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions));
-
-    $this->storage->expects($this->once())
-      ->method('getTableMapping')
-      ->will($this->returnValue($table_mapping));
-
     $expected = array(
       'entity_test' => array(
         'description' => 'The base table for entity_test entities.',
@@ -575,9 +562,17 @@ public function testGetSchemaTranslatable() {
       ),
     );
 
-    $actual = $this->schemaHandler->getSchema();
+    $this->setUpEntitySchemaHandler($expected);
+
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions);
+    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
+    $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions));
 
-    $this->assertEquals($expected, $actual);
+    $this->storage->expects($this->any())
+      ->method('getTableMapping')
+      ->will($this->returnValue($table_mapping));
+
+    $this->schemaHandler->createEntitySchema($this->entityType);
   }
 
   /**
@@ -585,7 +580,7 @@ public function testGetSchemaTranslatable() {
    *
    * @covers ::__construct()
    * @covers ::getSchema()
-   * @covers ::getTables()
+   * @covers ::getEntitySchemaTables()
    * @covers ::initializeDataTable()
    * @covers ::addTableDefaults()
    * @covers ::getEntityIndexName()
@@ -626,18 +621,6 @@ public function testGetSchemaRevisionableTranslatable() {
       ),
     ));
 
-    $this->setUpSchemaHandler();
-
-    $table_mapping = new DefaultTableMapping($this->storageDefinitions);
-    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
-    $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions));
-    $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions));
-    $table_mapping->setFieldNames('entity_test_revision_field_data', array_keys($this->storageDefinitions));
-
-    $this->storage->expects($this->once())
-      ->method('getTableMapping')
-      ->will($this->returnValue($table_mapping));
-
     $expected = array(
       'entity_test' => array(
         'description' => 'The base table for entity_test entities.',
@@ -763,26 +746,296 @@ public function testGetSchemaRevisionableTranslatable() {
       ),
     );
 
-    $actual = $this->schemaHandler->getSchema();
+    $this->setUpEntitySchemaHandler($expected);
+
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions);
+    $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions));
+    $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions));
+    $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions));
+    $table_mapping->setFieldNames('entity_test_revision_field_data', array_keys($this->storageDefinitions));
+
+    $this->storage->expects($this->any())
+      ->method('getTableMapping')
+      ->will($this->returnValue($table_mapping));
+
+    $this->schemaHandler->createEntitySchema($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' => false,
+            'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned',
+          ),
+          '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',
+            ),
+          ),
+        ),
+      ),
+      $entity_type_id . '_revision__' . $field_name => array(
+        'description' => "Revision archive 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',
+          ),
+          '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', 'revision_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->setUpEntitySchemaHandler($expected);
 
-    $this->assertEquals($expected, $actual);
+    $table_mapping = new DefaultTableMapping($this->storageDefinitions, $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->schemaHandler->createFieldSchema($field_storage);
   }
 
   /**
    * Sets up the schema handler.
    *
-   * This uses the field definitions set in $this->fieldDefinitions.
+   * This uses the field definitions set in $this->storageDefinitions.
+   *
+   * @param array $expected
+   *   (optional) An associative array describing the expected entity schema to
+   *   be created. Defaults to expecting nothing.
    */
-  protected function setUpSchemaHandler() {
-    $this->entityManager->expects($this->once())
+  protected function setUpEntitySchemaHandler(array $expected = array()) {
+    $this->entityManager->expects($this->any())
+      ->method('getDefinition')
+      ->with($this->entityType->id())
+      ->will($this->returnValue($this->entityType));
+
+    $this->entityManager->expects($this->any())
       ->method('getFieldStorageDefinitions')
       ->with($this->entityType->id())
       ->will($this->returnValue($this->storageDefinitions));
-    $this->schemaHandler = new ContentEntitySchemaHandler(
-      $this->entityManager,
-      $this->entityType,
-      $this->storage
-    );
+
+    $db_schema_handler = $this->getMockBuilder('Drupal\Core\Database\Schema')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    if ($expected) {
+      $invocation_count = 0;
+      $expected_table_names = array_keys($expected);
+      $expected_table_schemas = array_values($expected);
+
+      $db_schema_handler->expects($this->any())
+        ->method('createTable')
+        ->with(
+          $this->callback(function($table_name) use (&$invocation_count, $expected_table_names) {
+            return $expected_table_names[$invocation_count] == $table_name;
+          }),
+          $this->callback(function($table_schema) use (&$invocation_count, $expected_table_schemas) {
+            return $expected_table_schemas[$invocation_count] == $table_schema;
+          })
+        )
+        ->will($this->returnCallback(function() use (&$invocation_count) {
+          $invocation_count++;
+        }));
+    }
+
+    $connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $connection->expects($this->any())
+      ->method('schema')
+      ->will($this->returnValue($db_schema_handler));
+
+    $state = $this->getMock('Drupal\Core\State\StateInterface');
+    $this->schemaHandler = $this->getMockBuilder('Drupal\Core\Entity\Schema\ContentEntitySchemaHandler')
+      ->setConstructorArgs(array($this->entityManager, $this->entityType, $this->storage, $connection))
+      ->setMethods(array('state'))
+      ->getMock();
+    $this->schemaHandler
+      ->expects($this->any())
+      ->method('state')
+      ->will($this->returnValue($state));
   }
 
   /**
@@ -795,7 +1048,11 @@ protected function setUpSchemaHandler() {
    *   FieldStorageDefinitionInterface::getSchema().
    */
   public function setUpStorageDefinition($field_name, array $schema) {
-    $this->storageDefinitions[$field_name] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+    $this->storageDefinitions[$field_name] = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');
+    // getDescription() is called once for each table.
+    $this->storageDefinitions[$field_name]->expects($this->any())
+      ->method('getName')
+      ->will($this->returnValue($field_name));
     // getDescription() is called once for each table.
     $this->storageDefinitions[$field_name]->expects($this->any())
       ->method('getDescription')
@@ -804,7 +1061,7 @@ public function setUpStorageDefinition($field_name, array $schema) {
     $this->storageDefinitions[$field_name]->expects($this->any())
       ->method('getSchema')
       ->will($this->returnValue($schema));
-    $this->storageDefinitions[$field_name]->expects($this->once())
+    $this->storageDefinitions[$field_name]->expects($this->any())
       ->method('getColumns')
       ->will($this->returnValue($schema['columns']));
   }
diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php
index 18802c7..baf360b 100644
--- a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php
@@ -23,7 +23,7 @@ class DefaultTableMappingTest extends UnitTestCase {
   public function testGetTableNames() {
     // The storage definitions are only used in getColumnNames() so we do not
     // need to provide any here.
-    $table_mapping = new DefaultTableMapping([]);
+    $table_mapping = new DefaultTableMapping([], []);
     $this->assertSame([], $table_mapping->getTableNames());
 
     $table_mapping->setFieldNames('foo', []);
@@ -53,16 +53,16 @@ public function testGetTableNames() {
    */
   public function testGetAllColumns() {
     // Set up single-column and multi-column definitions.
-    $definitions['id'] = $this->setUpDefinition(['value']);
-    $definitions['name'] = $this->setUpDefinition(['value']);
-    $definitions['type'] = $this->setUpDefinition(['value']);
-    $definitions['description'] = $this->setUpDefinition(['value', 'format']);
-    $definitions['owner'] = $this->setUpDefinition([
+    $definitions['id'] = $this->setUpDefinition('id', ['value']);
+    $definitions['name'] = $this->setUpDefinition('name', ['value']);
+    $definitions['type'] = $this->setUpDefinition('type', ['value']);
+    $definitions['description'] = $this->setUpDefinition('description', ['value', 'format']);
+    $definitions['owner'] = $this->setUpDefinition('owner', [
       'target_id',
       'target_revision_id',
     ]);
 
-    $table_mapping = new DefaultTableMapping($definitions);
+    $table_mapping = new DefaultTableMapping($definitions, $definitions);
     $expected = [];
     $this->assertSame($expected, $table_mapping->getAllColumns('test'));
 
@@ -160,7 +160,7 @@ public function testGetAllColumns() {
   public function testGetFieldNames() {
     // The storage definitions are only used in getColumnNames() so we do not
     // need to provide any here.
-    $table_mapping = new DefaultTableMapping([]);
+    $table_mapping = new DefaultTableMapping([], []);
 
     // Test that requesting the list of field names for a table for which no
     // fields have been added does not fail.
@@ -188,18 +188,18 @@ public function testGetFieldNames() {
    * @covers ::getColumnNames()
    */
   public function testGetColumnNames() {
-    $definitions['test'] = $this->setUpDefinition([]);
-    $table_mapping = new DefaultTableMapping($definitions);
+    $definitions['test'] = $this->setUpDefinition('test', []);
+    $table_mapping = new DefaultTableMapping($definitions, $definitions);
     $expected = [];
     $this->assertSame($expected, $table_mapping->getColumnNames('test'));
 
-    $definitions['test'] = $this->setUpDefinition(['value']);
-    $table_mapping = new DefaultTableMapping($definitions);
+    $definitions['test'] = $this->setUpDefinition('test', ['value']);
+    $table_mapping = new DefaultTableMapping($definitions, $definitions);
     $expected = ['value' => 'test'];
     $this->assertSame($expected, $table_mapping->getColumnNames('test'));
 
-    $definitions['test'] = $this->setUpDefinition(['value', 'format']);
-    $table_mapping = new DefaultTableMapping($definitions);
+    $definitions['test'] = $this->setUpDefinition('test', ['value', 'format']);
+    $table_mapping = new DefaultTableMapping($definitions, $definitions);
     $expected = ['value' => 'test__value', 'format' => 'test__format'];
     $this->assertSame($expected, $table_mapping->getColumnNames('test'));
   }
@@ -213,7 +213,7 @@ public function testGetColumnNames() {
   public function testGetExtraColumns() {
     // The storage definitions are only used in getColumnNames() so we do not
     // need to provide any here.
-    $table_mapping = new DefaultTableMapping([]);
+    $table_mapping = new DefaultTableMapping([], []);
 
     // Test that requesting the list of field names for a table for which no
     // fields have been added does not fail.
@@ -237,13 +237,18 @@ public function testGetExtraColumns() {
   /**
    * Sets up a field storage definition for the test.
    *
+   * @param string $name
+   *   The field name.
    * @param array $column_names
    *   An array of column names for the storage definition.
    *
    * @return \Drupal\Core\Field\FieldStorageDefinitionInterface|\PHPUnit_Framework_MockObject_MockObject
    */
-  protected function setUpDefinition(array $column_names) {
-    $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
+  protected function setUpDefinition($name, array $column_names) {
+    $definition = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');
+    $definition->expects($this->any())
+      ->method('getName')
+      ->will($this->returnValue($name));
     $definition->expects($this->any())
       ->method('getColumns')
       ->will($this->returnValue(array_fill_keys($column_names, [])));
diff --git a/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php b/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php
new file mode 100644
index 0000000..f685edd
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface.
+ */
+
+namespace Drupal\Tests\Core\Field;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
+/**
+ * Defines a test interface to mock entity base field definitions.
+ */
+interface TestBaseFieldDefinitionInterface extends FieldDefinitionInterface, FieldStorageDefinitionInterface {
+}
diff --git a/core/update.php b/core/update.php
index 4e8157c..3376a2a 100644
--- a/core/update.php
+++ b/core/update.php
@@ -51,12 +51,22 @@
 /**
  * Renders a form with a list of available database updates.
  */
+function update_entity_schema_page() {
+  $build = \Drupal::formBuilder()->getForm('Drupal\Core\Update\Form\UpdateEntitySchemaForm');
+  $build['#title'] = 'Drupal entity schema updates';
+  return $build;
+}
+
+/**
+ * Renders a form with a list of available database updates.
+ */
 function update_selection_page() {
   // Make sure there is no stale theme registry.
   \Drupal::cache()->deleteAll();
 
-  $build = \Drupal::formBuilder()->getForm('Drupal\Core\Update\Form\UpdateScriptSelectionForm');
-  $build['#title'] = 'Drupal database update';
+  $force_updates = (bool) \Drupal::request()->get('entity_schema_updates');
+  $build = \Drupal::formBuilder()->getForm('Drupal\Core\Update\Form\UpdateScriptSelectionForm', $force_updates);
+  $build['#title'] = 'Drupal module updates';
 
   return $build;
 }
@@ -198,6 +208,9 @@ function update_info_page() {
   $keyvalue->get('update_available_release')->deleteAll();
 
   $token = \Drupal::csrfToken()->get('update');
+  $entity_schema_updates = count(\Drupal::service('entity.schema.manager')->getChangeList());
+  $op = $entity_schema_updates ? 'entity_schema' : 'selection';
+
   $output = '<p>Use this utility to update your database whenever a new release of Drupal or a module is installed.</p><p>For more detailed information, see the <a href="http://drupal.org/upgrade">upgrading handbook</a>. If you are unsure what these terms mean you should probably contact your hosting provider.</p>';
   $output .= "<ol>\n";
   $output .= "<li><strong>Back up your code</strong>. Hint: when backing up module code, do not leave that backup in the 'modules' or 'sites/*/modules' directories as this may confuse Drupal's auto-discovery mechanism.</li>\n";
@@ -206,7 +219,7 @@ function update_info_page() {
   $output .= "<li>Install your new files in the appropriate location, as described in the handbook.</li>\n";
   $output .= "</ol>\n";
   $output .= "<p>When you have performed the steps above, you may proceed.</p>\n";
-  $form_action = check_url(drupal_current_script_url(array('op' => 'selection', 'token' => $token)));
+  $form_action = check_url(drupal_current_script_url(array('op' => $op, 'token' => $token)));
   $output .= '<form method="post" action="' . $form_action . '"><div class="form-actions form-wrapper" id="edit-actions"><input type="submit" value="Continue" class="button button--primary form-submit" /></div></form>';
   $output .= "\n";
 
@@ -260,7 +273,8 @@ function update_task_list($active = NULL) {
   $tasks = array(
     'requirements' => 'Verify requirements',
     'info' => 'Overview',
-    'select' => 'Review updates',
+    'entity_schema' => 'Review entity schema updates',
+    'selection' => 'Review module updates',
     'run' => 'Run updates',
     'finished' => 'Review log',
   );
@@ -369,11 +383,13 @@ function update_task_list($active = NULL) {
   switch ($op) {
     // update.php ops.
 
+    case 'entity_schema':
     case 'selection':
       $token = $request->query->get('token');
       if (isset($token) && \Drupal::csrfToken()->validate($token, 'update')) {
-        $regions['sidebar_first'] = update_task_list('select');
-        $output = update_selection_page();
+        $regions['sidebar_first'] = update_task_list($op);
+        $task_callback = 'update_' . $op . '_page';
+        $output = $task_callback();
         break;
       }
 
