 core/includes/install.core.inc                     |   2 +-
 .../Core/Entity/ContentEntityDatabaseStorage.php   | 463 ++++++++++++++++----
 .../ContentEntityDatabaseStorageInterface.php      | 103 +++++
 .../Core/Entity/ContentEntityStorageBase.php       |   7 +
 .../Core/Entity/ContentEntityTypeInterface.php     |   2 +-
 core/lib/Drupal/Core/Entity/Entity.php             |   1 +
 core/lib/Drupal/Core/Entity/Query/Sql/Tables.php   |  52 ++-
 .../Entity/Schema/ContentEntitySchemaHandler.php   | 474 +++++++++++++++++++++
 .../Schema/ContentEntitySchemaHandlerInterface.php |  21 +
 .../lib/Drupal/Core/Entity/SqlStorageInterface.php |  24 ++
 .../Plugin/Field/FieldType/EntityReferenceItem.php |   8 +
 core/modules/aggregator/aggregator.install         | 176 --------
 .../lib/Drupal/aggregator/Entity/Feed.php          |   6 +-
 .../lib/Drupal/aggregator/Entity/Item.php          |   5 +-
 .../aggregator/Tests/Views/IntegrationTest.php     |   3 +-
 .../block/custom_block/custom_block.install        | 117 -----
 core/modules/comment/comment.install               | 226 ----------
 .../comment/lib/Drupal/comment/CommentStorage.php  |  98 ++++-
 .../comment/lib/Drupal/comment/Entity/Comment.php  |   3 +-
 .../Drupal/comment/Tests/CommentValidationTest.php |   2 +-
 .../config/Tests/ConfigImportRecreateTest.php      |   4 +-
 .../edit/lib/Drupal/edit/Tests/EditTestBase.php    |   2 +-
 .../Drupal/editor/Tests/EditorFileUsageTest.php    |   4 +-
 core/modules/entity/entity.module                  |  60 +++
 .../lib/Drupal/entity/Tests/EntityDisplayTest.php  |   2 +-
 .../Tests/EntityReferenceFieldTest.php             |   2 +-
 .../Tests/EntityReferenceItemTest.php              |   3 +-
 .../Drupal/field/Tests/FieldAttachOtherTest.php    |   2 +-
 .../Drupal/field/Tests/FieldAttachStorageTest.php  |   2 +-
 .../field/lib/Drupal/field/Tests/FieldInfoTest.php |   1 -
 .../lib/Drupal/field/Tests/FieldUnitTestBase.php   |   4 +-
 .../lib/Drupal/field/Tests/TranslationWebTest.php  |   4 +-
 core/modules/file/file.install                     | 151 -------
 core/modules/file/file.module                      |   1 +
 core/modules/file/lib/Drupal/file/FileStorage.php  |  56 +++
 .../file/lib/Drupal/file/Tests/FileItemTest.php    |   3 +-
 .../lib/Drupal/file/Tests/FileManagedTestBase.php  |  25 +-
 .../Drupal/file/Tests/FileManagedUnitTestBase.php  |  25 +-
 .../file/lib/Drupal/file/Tests/SpaceUsedTest.php   |  44 +-
 core/modules/file/tests/file_test/file_test.module |   8 +-
 .../filter/Tests/FilterDefaultConfigTest.php       |   2 +-
 core/modules/forum/forum.install                   |  27 +-
 .../hal/lib/Drupal/hal/Tests/EntityTest.php        |  10 +-
 .../lib/Drupal/hal/Tests/NormalizerTestBase.php    |   4 +-
 .../image/lib/Drupal/image/Tests/ImageItemTest.php |   2 +-
 core/modules/node/lib/Drupal/node/Entity/Node.php  |  24 +-
 .../node/lib/Drupal/node/NodeFormController.php    |  15 +-
 core/modules/node/lib/Drupal/node/NodeStorage.php  | 100 +++++
 .../node/Tests/Condition/NodeConditionTest.php     |   2 +-
 .../node/Tests/Config/NodeImportCreateTest.php     |   2 +-
 .../lib/Drupal/node/Tests/NodeFormButtonsTest.php  |   5 +-
 .../lib/Drupal/node/Tests/NodeLastChangedTest.php  |   7 +-
 .../lib/Drupal/node/Tests/NodeTokenReplaceTest.php |   2 +-
 .../lib/Drupal/node/Tests/NodeValidationTest.php   |   2 +-
 core/modules/node/node.install                     | 413 +-----------------
 .../lib/Drupal/rdf/Tests/CommentAttributesTest.php |   2 +-
 .../Tests/Field/TaxonomyTermReferenceRdfaTest.php  |   2 +-
 .../rest/lib/Drupal/rest/Tests/UpdateTest.php      |   1 -
 .../Tests/EntitySerializationTest.php              |  12 +-
 .../serialization/Tests/NormalizerTestBase.php     |   4 +-
 .../lib/Drupal/shortcut/Entity/Shortcut.php        |   4 -
 core/modules/shortcut/shortcut.install             |  90 ----
 .../lib/Drupal/simpletest/DrupalUnitTestBase.php   |  33 ++
 .../simpletest/Tests/DrupalUnitTestBaseTest.php    |  25 +-
 .../simpletest_test/simpletest_test.info.yml       |   7 +
 .../simpletest_test/simpletest_test.install        |  50 +++
 .../Drupal/system/Tests/Action/ActionUnitTest.php  |   2 +-
 .../Drupal/system/Tests/Common/WriteRecordTest.php |  18 +-
 .../Drupal/system/Tests/Entity/EntityApiTest.php   |  14 +-
 .../system/Tests/Entity/EntityCrudHookTest.php     |  13 +-
 .../Drupal/system/Tests/Entity/EntityFieldTest.php |  18 +-
 .../system/Tests/Entity/EntityLanguageTestBase.php |  14 +-
 .../Tests/Entity/EntityQueryRelationshipTest.php   |   2 +-
 .../Drupal/system/Tests/Entity/EntityQueryTest.php |   4 +-
 .../Drupal/system/Tests/Entity/EntityUUIDTest.php  |  13 +-
 .../system/Tests/Entity/EntityUnitTestBase.php     |   6 +-
 .../system/Tests/Entity/EntityValidationTest.php   |  15 +-
 .../Drupal/system/Tests/Entity/FieldAccessTest.php |   3 +-
 .../system/Tests/Entity/FieldSqlStorageTest.php    |   3 +-
 .../system/Tests/TypedData/TypedDataTest.php       |   2 +-
 .../modules/database_test/database_test.install    |  29 ++
 .../tests/modules/entity_test/entity_test.install  | 403 ------------------
 .../lib/Drupal/entity_test/Entity/EntityTest.php   |   2 +-
 .../Entity/EntityTestBaseFieldDisplay.php          |   1 -
 .../entity_test/Entity/EntityTestLabelCallback.php |   1 -
 .../Drupal/entity_test/Entity/EntityTestMul.php    |  13 -
 .../Drupal/entity_test/Entity/EntityTestMulRev.php |  19 -
 .../taxonomy/lib/Drupal/taxonomy/TermStorage.php   |  86 ++++
 .../Tests/TaxonomyTermReferenceItemTest.php        |   3 +-
 .../Drupal/taxonomy/Tests/TermValidationTest.php   |   2 +-
 core/modules/taxonomy/taxonomy.install             | 164 -------
 .../text/Tests/Formatter/TextPlainUnitTest.php     |   2 +-
 .../Drupal/text/Tests/TextWithSummaryItemTest.php  |   2 +-
 core/modules/tracker/tracker.install               |  18 +-
 .../user/lib/Drupal/user/AccountFormController.php |   4 +-
 core/modules/user/lib/Drupal/user/Entity/User.php  |   2 +
 .../user/lib/Drupal/user/Tests/UserAdminTest.php   |  25 +-
 .../user/lib/Drupal/user/Tests/UserInstallTest.php |   5 +-
 .../lib/Drupal/user/Tests/UserValidationTest.php   |   2 +-
 .../Drupal/user/Tests/Views/UserUnitTestBase.php   |   2 +-
 core/modules/user/lib/Drupal/user/UserStorage.php  | 112 ++++-
 .../views.view.test_filter_permission.yml          |  15 +-
 core/modules/user/user.info.yml                    |   2 +
 core/modules/user/user.install                     | 265 ++----------
 .../modules/views/lib/Drupal/views/Entity/View.php |   5 +-
 .../views/Tests/Entity/RowEntityRenderersTest.php  |   4 +-
 .../Drupal/views/Tests/Handler/AreaTextTest.php    |   2 +-
 .../views/Tests/Handler/HandlerAliasTest.php       |   2 +-
 .../Tests/Plugin/RelationshipJoinTestBase.php      |   2 +-
 .../Drupal/views/Tests/Plugin/RowEntityTest.php    |   2 +-
 .../lib/Drupal/views/Tests/QueryGroupByTest.php    |   2 +-
 .../lib/Drupal/views/Tests/ViewExecutableTest.php  |   7 +-
 .../Schema/ContentEntitySchemaHandlerTest.php      | 222 ++++++++++
 113 files changed, 2250 insertions(+), 2347 deletions(-)

diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index aa158a2..bf19f83 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -969,7 +969,7 @@ function install_base_system(&$install_state) {
 
   // Enable the user module so that sessions can be recorded during the
   // upcoming bootstrap step.
-  \Drupal::moduleHandler()->install(array('user'), FALSE);
+  \Drupal::moduleHandler()->install(array('user'));
 
   // Save the list of other modules to install for the upcoming tasks.
   // State can be set to the database now that system.module is installed.
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
index cc97849..9fca824 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
@@ -8,7 +8,11 @@
 namespace Drupal\Core\Entity;
 
 use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\Database;
 use Drupal\Core\Entity\Query\QueryInterface;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandlerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Language\Language;
 use Drupal\field\FieldInfo;
 use Drupal\field\FieldConfigUpdateForbiddenException;
@@ -25,7 +29,28 @@
  * This class can be used as-is by most simple entity types. Entity types
  * requiring special handling can extend the class.
  */
-class ContentEntityDatabaseStorage extends ContentEntityStorageBase {
+class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements ContentEntityDatabaseStorageInterface, ContentEntitySchemaHandlerInterface {
+
+  /**
+   * The base field definitions for this entity type.
+   *
+   * @var \Drupal\Core\Field\FieldDefinitionInterface[]
+   */
+  protected $fieldDefinitions;
+
+  /**
+   * The table layout associated to the entity type.
+   *
+   * @var int
+   */
+  protected $layoutType;
+
+  /**
+   * A mapping of schema fields that will be stored per entity table.
+   *
+   * @var array
+   */
+  protected $tableMapping;
 
   /**
    * Name of entity's revision database table field, if it supports revisions.
@@ -37,6 +62,22 @@ class ContentEntityDatabaseStorage extends ContentEntityStorageBase {
   protected $revisionKey = FALSE;
 
   /**
+   * Name of entity's default_langcode database table field.
+   *
+   * Has the value FALSE if this entity does not support multilingual storage.
+   *
+   * @var string|bool
+   */
+  protected $defaultLangcodeKey = FALSE;
+
+  /**
+   * The base table of the entity, if the entity has storage.
+   *
+   * @var string
+   */
+  protected $baseTable;
+
+  /**
    * The table that stores revisions, if the entity supports revisions.
    *
    * @var string
@@ -79,12 +120,27 @@ class ContentEntityDatabaseStorage extends ContentEntityStorageBase {
   protected $fieldInfo;
 
   /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * The entity schema builder.
+   *
+   * @var \Drupal\Core\Entity\Schema\ContentEntitySchemaHandlerInterface
+   */
+  protected $schemaHandler;
+
+  /**
    * {@inheritdoc}
    */
   public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
     return new static(
       $entity_type,
       $container->get('database'),
+      $container->get('entity.manager'),
       $container->get('field.info')
     );
   }
@@ -96,38 +152,248 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    *   The entity type definition.
    * @param \Drupal\Core\Database\Connection $database
    *   The database connection to be used.
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
    * @param \Drupal\field\FieldInfo $field_info
-   *   The field info service.
+   *   (optional) The field info service. Defaults to NULL as storage can be
+   *   instantiated before Field module is installed.
    */
-  public function __construct(EntityTypeInterface $entity_type, Connection $database, FieldInfo $field_info) {
+  public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, FieldInfo $field_info = NULL) {
     parent::__construct($entity_type);
 
     $this->database = $database;
     $this->fieldInfo = $field_info;
+    $this->entityManager = $entity_manager;
+    $this->fieldDefinitions = $entity_manager->getBaseFieldDefinitions($entity_type->id());
 
-    // Check if the entity type supports IDs.
-    if ($this->entityType->hasKey('id')) {
-      $this->idKey = $this->entityType->getKey('id');
+    $this->initTableLayout();
+  }
+
+  /**
+   * Initializes table and key names based on the current layout type.
+   */
+  protected function initTableLayout() {
+    $this->idKey = $this->entityType->getKey('id') ?: 'id';
+    $this->uuidKey = $this->entityType->getKey('uuid') ?: 'uuid';
+
+    // The bundle key is optional.
+    $this->bundleKey = $this->entityType->getKey('bundle');
+
+    // @todo Table names do not belong to the entity type definition, they are
+    //   storage implementation details. Rip them out.
+    $this->baseTable = $this->entityType->getBaseTable() ?: $this->entityTypeId;
+
+    // Retrieve the current table layout type based on the entity type
+    // definition.
+    $layout_type = $this->getLayoutType();
+
+    if ($layout_type & static::LAYOUT_REVISION) {
+      $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
+      $this->revisionTable = $this->entityType->getRevisionTable() ?: $this->entityTypeId . '_revision';
     }
 
-    // Check if the entity type supports UUIDs.
-    $this->uuidKey = $this->entityType->getKey('uuid');
+    if ($layout_type & static::LAYOUT_MULTILINGUAL) {
+      $this->dataTable = $this->entityType->getDataTable() ?: $this->entityTypeId . '_field_data';
+      $this->langcodeKey = $this->entityType->getKey('langcode') ?: 'langcode';
+      $this->defaultLangcodeKey = $this->entityType->getKey('default_langcode') ?: 'default_langcode';
+    }
 
-    // Check if the entity type supports revisions.
-    if ($this->entityType->hasKey('revision')) {
-      $this->revisionKey = $this->entityType->getKey('revision');
-      $this->revisionTable = $this->entityType->getRevisionTable();
+    if ($layout_type == static::LAYOUT_MULTILINGUAL_REVISION) {
+      $this->revisionDataTable = $this->entityType->getRevisionDataTable() ?: $this->entityTypeId . '_field_revision';
     }
+  }
 
-    // Check if the entity type has a dedicated table for fields.
-    if ($data_table = $this->entityType->getDataTable()) {
-      $this->dataTable = $data_table;
-      // Entity types having both revision and translation support should always
-      // define a revision data table.
-      if ($this->revisionTable && $revision_data_table = $this->entityType->getRevisionDataTable()) {
-        $this->revisionDataTable = $revision_data_table;
+  /**
+   * {@inheritdoc}
+   */
+  public function getLayoutType() {
+    if (!isset($this->layoutType)) {
+      $this->layoutType = static::LAYOUT_BASE;
+      if ($this->entityType->hasKey('revision')) {
+        $this->layoutType |= static::LAYOUT_REVISION;
+      }
+      // @todo Remove the data table check once all entity types are using
+      //   entity query and we have a views data controller. See:
+      //   - https://drupal.org/node/2068325
+      //   - https://drupal.org/node/1740492
+      $data_table = $this->entityType->getDataTable();
+      if ($data_table && $this->entityType->isTranslatable()) {
+        $this->layoutType |= static::LAYOUT_MULTILINGUAL;
       }
     }
+    return $this->layoutType;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBaseTable() {
+    return $this->baseTable;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRevisionTable() {
+    return $this->revisionTable;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDataTable() {
+    return $this->dataTable;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRevisionDataTable() {
+    return $this->revisionDataTable;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSchema() {
+    $schema = $this->buildSchema();
+    $context = array('entity_type' => $this->entityType);
+    // TODO Document this hook.
+    $this->moduleHandler()->alter('entity_schema', $schema, $context);
+    return $schema;
+  }
+
+  /**
+   * Builds the database schema for the related entity type.
+   *
+   * @return
+   *   A schema API array.
+   */
+  protected function buildSchema() {
+    return $this->schemaHandler()->getSchema();
+  }
+
+  /**
+   * Gets the schema handler for this storage controller.
+   *
+   * @return \Drupal\Core\Entity\Schema\ContentEntitySchemaHandler
+   *   The schema handler.
+   */
+  protected function schemaHandler() {
+    if (!isset($this->schemaHandler)) {
+      $this->schemaHandler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this);
+    }
+    return $this->schemaHandler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTableMapping() {
+    if (!isset($this->tableMapping)) {
+      $table_mapping = array();
+
+      $key_fields = array_filter(array($this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey));
+
+      // Storable fields are single-value base fields that are not defined as
+      // computed and that do not specify a custom format.
+      // @todo Add support for multiple-value base fields.
+      $storable_definitions = array_filter($this->fieldDefinitions, function (FieldDefinitionInterface $field_definition) {
+        return !$field_definition->isComputed() && !$field_definition->hasCustomStorage() && !$field_definition->isMultiple();
+      });
+      $storable_fields = array_keys($storable_definitions);
+
+      // @todo Provide automatic definitions for revision metadata fields.
+      //   Rename 'log' to 'revision_log'.
+      $revision_metadata_fields = array_intersect(array('revision_timestamp', 'revision_uid', 'log'), $storable_fields);
+      $revisionable_filter_callback = function (FieldDefinitionInterface $definition) { return $definition->isRevisionable(); };
+
+      switch ($this->getLayoutType()) {
+        // The base layout stores all the base field values in the base table.
+        case static::LAYOUT_BASE:
+          $table_mapping[$this->baseTable] = $this->processFields(array_merge($key_fields, array_diff($storable_fields, $key_fields)));
+          break;
+
+        // The base layout stores all the base field values in the base table.
+        // Revisionable fields are also stored in the revision table.
+        case static::LAYOUT_REVISION:
+          $table_mapping[$this->baseTable] = $this->processFields(array_merge($key_fields, array_diff($storable_fields, $key_fields, $revision_metadata_fields)));
+          $revision_key_fields = array($this->idKey, $this->revisionKey);
+          $revisionable_fields = array_keys(array_filter($storable_definitions, $revisionable_filter_callback));
+          $table_mapping[$this->revisionTable] = $this->processFields(array_merge($revision_key_fields, $revisionable_fields));
+          break;
+
+        // Multilingual layouts store key field values in the base table. The
+        // other base field values are stored in the data table, no matter
+        // whether they are translatable or not. The data table holds also a
+        // denormalized copy of the bundle field value to allow for more
+        // performant queries.
+        case static::LAYOUT_MULTILINGUAL:
+          $table_mapping[$this->baseTable] = $this->processFields($key_fields);
+          $data_key_fields = array_diff($key_fields, array($this->uuidKey));
+          $data_fields = array_diff($storable_fields, $key_fields);
+          $table_mapping[$this->dataTable] = $this->processFields(array_merge($data_key_fields, $data_fields));
+          // Add the denormalized 'default_langcode' field to the mapping. As it
+          // does not correspond to a field definition we add it with an empty
+          // key.
+          $table_mapping[$this->dataTable][''] = array('default_langcode');
+          break;
+
+        // The revisionable multilingual layout stores key field values in the
+        // base table, except for language, which is stored in the revision
+        // table along with revision metadata. The revision data table holds
+        // data field values for all the available revisions without
+        // denormalizations.
+        case static::LAYOUT_MULTILINGUAL_REVISION:
+          $table_mapping[$this->baseTable] = $this->processFields(array_diff($key_fields, array($this->langcodeKey)));
+          $data_key_fields = array_diff($key_fields, array($this->uuidKey));
+          $data_fields = array_diff($storable_fields, $key_fields, $revision_metadata_fields);
+          $table_mapping[$this->dataTable] = $this->processFields(array_merge($data_key_fields, $data_fields));
+          // Add the denormalized 'default_langcode' field to the mapping. As it
+          // does not correspond to a field definition we add it with an empty
+          // key.
+          $table_mapping[$this->dataTable][''] = array('default_langcode');
+          $table_mapping[$this->revisionTable] = $this->processFields(array_merge(array($this->idKey, $this->revisionKey, $this->langcodeKey), $revision_metadata_fields));
+          $revision_data_key_fields = array_diff($key_fields, array($this->bundleKey, $this->uuidKey));
+          $revisionable_fields = array_keys(array_filter($storable_definitions, $revisionable_filter_callback));
+          $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, $revision_data_key_fields);
+          $table_mapping[$this->revisionDataTable] = $this->processFields(array_merge($revision_data_key_fields, $revision_data_fields));
+          // See above.
+          $table_mapping[$this->revisionDataTable][''] = array('default_langcode');
+          break;
+      }
+
+      $this->tableMapping = $table_mapping;
+    }
+
+    return $this->tableMapping;
+  }
+
+  /**
+   * Returns a mapping between field and column names.
+   *
+   * @param array $field_names
+   *   An array of names of fields to map.
+   *
+   * @return array
+   *   An associative array of arrays of column names keyed by field name.
+   */
+  protected function processFields($field_names) {
+    $mapping = array();
+    foreach ($field_names as $field_name) {
+      $columns = isset($this->fieldDefinitions[$field_name]) ? array_keys($this->fieldDefinitions[$field_name]->getColumns()) : array();
+      // @todo Remove the entity reference check once we can handle schema
+      //   changes or the Entity reference module stops messing with field
+      //   schema. See:
+      //   - https://drupal.org/node/1498720
+      //   - https://drupal.org/node/2209981
+      foreach ($columns as $index => $column) {
+        if (!$index || $this->fieldDefinitions[$field_name]->getType() != 'entity_reference') {
+          $mapping[$field_name][] = $this->schemaHandler()->getFieldColumnName($this->fieldDefinitions[$field_name], $column);
+        }
+      }
+    }
+    return $mapping;
   }
 
   /**
@@ -265,15 +531,18 @@ protected function attachPropertyData(array &$entities) {
       }
 
       $data = $query->execute();
-      $field_definitions = \Drupal::entityManager()->getBaseFieldDefinitions($this->entityTypeId);
+      $field_definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
+
+      $table_mapping = $this->getTableMapping();
       $translations = array();
       if ($this->revisionDataTable) {
-        $data_column_names = array_flip(array_diff(drupal_schema_fields_sql($this->entityType->getRevisionDataTable()), drupal_schema_fields_sql($this->entityType->getBaseTable())));
+        $data_column_names = call_user_func_array('array_merge', array_diff_key($table_mapping[$this->revisionDataTable], $table_mapping[$this->baseTable]));
       }
       else {
-        $data_column_names = array_flip(drupal_schema_fields_sql($this->entityType->getDataTable()));
+        $data_column_names = call_user_func_array('array_merge', $table_mapping[$this->dataTable]);
       }
 
+      $data_column_names = array_combine($data_column_names, $data_column_names);
       foreach ($data as $values) {
         $id = $values[$this->idKey];
 
@@ -403,11 +672,12 @@ protected function buildQuery($ids, $revision_id = FALSE) {
     }
 
     // Add fields from the {entity} table.
-    $entity_fields = drupal_schema_fields_sql($this->entityType->getBaseTable());
+    $table_mapping = $this->getTableMapping();
+    $entity_fields = call_user_func_array('array_merge', $table_mapping[$this->baseTable]);
 
     if ($this->revisionTable) {
       // Add all fields from the {entity_revision} table.
-      $entity_revision_fields = drupal_schema_fields_sql($this->entityType->getRevisionTable());
+      $entity_revision_fields = call_user_func_array('array_merge', $table_mapping[$this->revisionTable]);
       $entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields);
       // The ID field is provided by entity, so remove it.
       unset($entity_revision_fields[$this->idKey]);
@@ -552,7 +822,12 @@ public function save(EntityInterface $entity) {
 
       if (!$entity->isNew()) {
         if ($entity->isDefaultRevision()) {
-          $return = drupal_write_record($this->entityType->getBaseTable(), $record, $this->idKey);
+          $this->database
+            ->update($this->baseTable)
+            ->fields((array) $record)
+            ->condition($this->idKey, $record->{$this->idKey})
+            ->execute();
+          $return = SAVED_UPDATED;
         }
         else {
           // @todo, should a different value be returned when saving an entity
@@ -560,13 +835,13 @@ public function save(EntityInterface $entity) {
           $return = FALSE;
         }
         if ($this->revisionTable) {
-          $record->{$this->revisionKey} = $this->saveRevision($entity);
+          $entity->{$this->revisionKey}->value = $this->saveRevision($entity);
         }
         if ($this->dataTable) {
           $this->savePropertyData($entity);
         }
         if ($this->revisionDataTable) {
-          $this->savePropertyData($entity, 'revision_data_table');
+          $this->savePropertyData($entity, $this->revisionDataTable);
         }
         if ($this->revisionTable) {
           $entity->setNewRevision(FALSE);
@@ -584,7 +859,17 @@ public function save(EntityInterface $entity) {
         // Ensure the entity is still seen as new after assigning it an id,
         // while storing its data.
         $entity->enforceIsNew();
-        $return = drupal_write_record($this->entityType->getBaseTable(), $record);
+        $insert_id = $this->database
+          ->insert($this->baseTable, array('return' => Database::RETURN_INSERT_ID))
+          ->fields((array) $record)
+          ->execute();
+        // Even if this is a new entity, the ID key might have been set in which
+        // case we should not override the provided ID. An empty value for the
+        // id is interpreted as NULL and thus overriden.
+        if (empty($record->{$this->idKey})) {
+          $record->{$this->idKey} = $insert_id;
+        }
+        $return = SAVED_NEW;
         $entity->{$this->idKey}->value = (string) $record->{$this->idKey};
         if ($this->revisionTable) {
           $entity->setNewRevision();
@@ -594,7 +879,7 @@ public function save(EntityInterface $entity) {
           $this->savePropertyData($entity);
         }
         if ($this->revisionDataTable) {
-          $this->savePropertyData($entity, 'revision_data_table');
+          $this->savePropertyData($entity, $this->revisionDataTable);
         }
 
         $entity->enforceIsNew(FALSE);
@@ -628,13 +913,14 @@ public function save(EntityInterface $entity) {
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   The entity object.
-   * @param string $table_key
-   *   (optional) The entity key identifying the target table. Defaults to
-   *   'data_table'.
+   * @param string $table_name
+   *   (optional) The table name to save to. Defaults to the data table.
    */
-  protected function savePropertyData(EntityInterface $entity, $table_key = 'data_table') {
-    $table_name = $this->entityType->get($table_key);
-    $revision = $table_key != 'data_table';
+  protected function savePropertyData(EntityInterface $entity, $table_name = NULL) {
+    if (!isset($table_name)) {
+      $table_name = $this->dataTable;
+    }
+    $revision = $table_name != $this->dataTable;
 
     if (!$revision || !$entity->isNewRevision()) {
       $key = $revision ? $this->revisionKey : $this->idKey;
@@ -649,7 +935,7 @@ protected function savePropertyData(EntityInterface $entity, $table_key = 'data_
 
     foreach ($entity->getTranslationLanguages() as $langcode => $language) {
       $translation = $entity->getTranslation($langcode);
-      $record = $this->mapToDataStorageRecord($translation, $table_key);
+      $record = $this->mapToDataStorageRecord($translation, $table_name);
       $values = (array) $record;
       $query
         ->fields(array_keys($values))
@@ -662,53 +948,58 @@ protected function savePropertyData(EntityInterface $entity, $table_key = 'data_
   /**
    * Maps from an entity object to the storage record.
    *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
    *   The entity object.
-   * @param string $table_key
-   *   (optional) The entity key identifying the target table. Defaults to
-   *   'base_table'.
+   * @param string $table_name
+   *   (optional) The table name to map records to. Defaults to the base table.
    *
    * @return \stdClass
    *   The record to store.
    */
-  protected function mapToStorageRecord(EntityInterface $entity, $table_key = 'base_table') {
+  protected function mapToStorageRecord(ContentEntityInterface $entity, $table_name = NULL) {
+    if (!isset($table_name)) {
+      $table_name = $this->baseTable;
+    }
+
     $record = new \stdClass();
-    $values = array();
-    $definitions = $entity->getFieldDefinitions();
-    $schema = drupal_get_schema($this->entityType->get($table_key));
     $is_new = $entity->isNew();
+    $table_mapping = $this->getTableMapping();
+
+    // @todo We still need to retrieve the schema as long as we have the BC
+    //   layer in place.
+    $schema = drupal_get_schema($table_name);
+
+    foreach ($table_mapping[$table_name] as $name => $storage_columns) {
+      if (!empty($this->fieldDefinitions[$name])) {
+        $definition = $this->fieldDefinitions[$name];
+        foreach ($definition->getColumns() as $column => $column_info) {
+          $value = isset($entity->$name->$column) ? $entity->$name->$column : NULL;
+
+          // We allow for storage controllers to have schema fields which are
+          // not entity fields, which can be useful for normalization purposes.
+          // The 'default_langcode' field, for example, is simply a
+          // denormalization of checking the language code in the data table
+          // against the language code in the base table and similarly the
+          // 'isDefaultRevision' field (which we add as a query expression in
+          // self::buildQuery()) is denormalization of checking the revision ID
+          // in the revision table against the revision ID in the base table.
+          if (!empty($column_info['serialize'])) {
+            $value = serialize($value);
+          }
 
-    $multi_column_fields = array();
-    foreach (drupal_schema_fields_sql($this->entityType->get($table_key)) as $name) {
-      // Check for fields which store data in multiple columns and process them
-      // separately.
-      if ($field = strstr($name, '__', TRUE)) {
-        $multi_column_fields[$field] = TRUE;
-        continue;
-      }
-      $values[$name] = isset($definitions[$name]) && isset($entity->$name->value) ? $entity->$name->value : NULL;
-    }
-
-    // Handle fields that store multiple properties and match each property name
-    // to its schema column name.
-    foreach (array_keys($multi_column_fields) as $field_name) {
-      $field_items = $entity->get($field_name);
-      $field_value = $field_items->getValue();
-      foreach (array_keys($field_items->getFieldDefinition()->getColumns()) as $field_schema_column) {
-        if (isset($schema['fields'][$field_name . '__' . $field_schema_column])) {
-          $values[$field_name . '__' . $field_schema_column] = isset($field_value[0][$field_schema_column]) ? $field_value[0][$field_schema_column] : NULL;
+          // If we are creating a new entity, we must not populate the record
+          // with NULL values otherwise defaults would not be applied.
+          if (isset($value) || !$is_new) {
+            $info = isset($schema['fields'][$name]) ? $schema['fields'][$name] : $definition->getSchema()['columns'][$column];
+            list(, $storage_column) = each($storage_columns);
+            if (!empty($storage_column)) {
+              $record->$storage_column = drupal_schema_get_field_value($info, $value);
+            }
+          }
         }
       }
     }
 
-    foreach ($values as $field_name => $value) {
-      // If we are creating a new entity, we must not populate the record with
-      // NULL values otherwise defaults would not be applied.
-      if (isset($value) || !$is_new) {
-        $record->$field_name = drupal_schema_get_field_value($schema['fields'][$field_name], $value);
-      }
-    }
-
     return $record;
   }
 
@@ -717,15 +1008,17 @@ protected function mapToStorageRecord(EntityInterface $entity, $table_key = 'bas
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   The entity object.
-   * @param string $table_key
-   *   (optional) The entity key identifying the target table. Defaults to
-   *   'data_table'.
+   * @param string $table_name
+   *   (optional) The table name to map records to. Defaults to the data table.
    *
    * @return \stdClass
    *   The record to store.
    */
-  protected function mapToDataStorageRecord(EntityInterface $entity, $table_key = 'data_table') {
-    $record = $this->mapToStorageRecord($entity, $table_key);
+  protected function mapToDataStorageRecord(EntityInterface $entity, $table_name = NULL) {
+    if (!isset($table_name)) {
+      $table_name = $this->dataTable;
+    }
+    $record = $this->mapToStorageRecord($entity, $table_name);
     $record->langcode = $entity->language()->id;
     $record->default_langcode = intval($record->langcode == $entity->getUntranslated()->language()->id);
     return $record;
@@ -741,12 +1034,20 @@ protected function mapToDataStorageRecord(EntityInterface $entity, $table_key =
    *   The revision id.
    */
   protected function saveRevision(EntityInterface $entity) {
-    $record = $this->mapToStorageRecord($entity, 'revision_table');
+    $record = $this->mapToStorageRecord($entity, $this->revisionTable);
 
     $entity->preSaveRevision($this, $record);
 
     if ($entity->isNewRevision()) {
-      drupal_write_record($this->revisionTable, $record);
+      $insert_id = $this->database
+        ->insert($this->revisionTable, array('return' => Database::RETURN_INSERT_ID))
+        ->fields((array) $record)
+        ->execute();
+      // Even if this is a new revsision, the revision ID key might have been
+      // set in which case we should not override the provided revision ID.
+      if (!isset($record->{$this->revisionKey})) {
+        $record->{$this->revisionKey} = $insert_id;
+      }
       if ($entity->isDefaultRevision()) {
         $this->database->update($this->entityType->getBaseTable())
           ->fields(array($this->revisionKey => $record->{$this->revisionKey}))
@@ -755,7 +1056,11 @@ protected function saveRevision(EntityInterface $entity) {
       }
     }
     else {
-      drupal_write_record($this->revisionTable, $record, $this->revisionKey);
+      $this->database
+        ->update($this->revisionTable)
+        ->fields((array) $record)
+        ->condition($this->revisionKey, $record->{$this->revisionKey})
+        ->execute();
     }
 
     // Make sure to update the new revision key for the entity.
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorageInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorageInterface.php
new file mode 100644
index 0000000..1f31ec9
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorageInterface.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\ContentEntityDatabaseStorageInterface.
+ */
+
+namespace Drupal\Core\Entity;
+
+/**
+ * Defines an interface for content entity database storages.
+ *
+ * In contrast to SqlStorageInterface, which does not make any assumptions about
+ * the specific storage layout used, this interface assumes the following:
+ * - The entity storage fundamentally differs between revisionable and
+ *   non-revisionable entities and between translatable and non-translatable
+ *   entity types. In combination this constitutes four storage layout types
+ *   indicated by the four LAYOUT_* constants.
+ * - Depending on the storage layout entity data is stored in the following four
+ *   tables or a subset thereof:
+ *   - The base table
+ *   - The revision table
+ *   - The data table
+ *   - The revision data table
+ *
+ * @see \Drupal\Core\Entity\SqlStorageInterface
+ */
+interface ContentEntityDatabaseStorageInterface extends SqlStorageInterface {
+
+  /**
+   * The base table layout: no revision or multilingual support for base fields.
+   *
+   * @var int
+   */
+  const LAYOUT_BASE = 0;
+
+  /**
+   * The revision table layout: provides revision support for base fields.
+   *
+   * @var int
+   */
+  const LAYOUT_REVISION = 1;
+
+  /**
+   * The multilingual table layout: provides multilingual support for base
+   * fields.
+   *
+   * @var int
+   */
+  const LAYOUT_MULTILINGUAL = 2;
+
+  /**
+   * The multilingual revision table layout: provides revision and multilingual
+   * support for base fields.
+   *
+   * @var int
+   */
+  const LAYOUT_MULTILINGUAL_REVISION = 3;
+
+  /**
+   * Returns the current layout type.
+   *
+   * @return int
+   *   An integer value identifying the current layout type.
+   *
+   * @see static::LAYOUT_BASE
+   * @see static::LAYOUT_REVISION
+   * @see static::LAYOUT_MULTILINGUAL
+   * @see static::LAYOUT_MULTILINGUAL_REVISION
+   */
+  public function getLayoutType();
+
+  /**
+   * Returns the base table name.
+   *
+   * @return string
+   *   The table name.
+   */
+  public function getBaseTable();
+
+  /**
+   * Returns the revision table name.
+   *
+   * @return string|false
+   *   The table name or FALSE if it is not available.
+   */
+  public function getRevisionTable();
+  /**
+   * Returns the data table name.
+   *
+   * @return string|false
+   *   The table name or FALSE if it is not available.
+   */
+  public function getDataTable();
+
+  /**
+   * Returns the revision data table name.
+   *
+   * @return string|false
+   *   The table name or FALSE if it is not available.
+   */
+  public function getRevisionDataTable();
+
+}
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
index 0c2bfe4..469047c 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -23,6 +23,13 @@
   protected $bundleKey = FALSE;
 
   /**
+   * The entity langcode key
+   *
+   * @var string|bool
+   */
+  protected $langcodeKey = FALSE;
+
+  /**
    * Name of the entity class.
    *
    * @var string
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityTypeInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityTypeInterface.php
index e94488d..7a1558d 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityTypeInterface.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityTypeInterface.php
@@ -10,6 +10,6 @@
 /**
  * Provides an interface for a content entity type.
  */
-interface ContentEntityTypeInterface {
+interface ContentEntityTypeInterface extends EntityTypeInterface {
 
 }
diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 2300f60..82ef0d0 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -432,6 +432,7 @@ protected function onUpdateBundleEntity() {
     $bundle_of = $this->getEntityType()->getBundleOf();
     $entity_manager = \Drupal::entityManager();
     if ($bundle_of !== FALSE && $entity_manager->hasController($bundle_of, 'view_builder')) {
+      $entity_manager->clearCachedFieldDefinitions();
       $entity_manager->getViewBuilder($bundle_of)->resetCache();
     }
   }
diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
index ee2bebc..2ca76ad 100644
--- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
@@ -12,8 +12,10 @@
 use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\Core\Entity\Plugin\DataType\EntityReference;
 use Drupal\Core\Entity\Query\QueryException;
+use Drupal\Core\Entity\SqlStorageInterface;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Field as FieldInfo;
+use Drupal\Core\Entity\EntityType;
 
 /**
  * Adds tables and fields to the SQL entity query.
@@ -35,7 +37,6 @@ class Tables implements TablesInterface {
    */
   protected $entityTables = array();
 
-
   /**
    * Field table array, key is table name, value is alias.
    *
@@ -46,10 +47,18 @@ class Tables implements TablesInterface {
   protected $fieldTables = array();
 
   /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManager
+   */
+  protected $entityManager;
+
+  /**
    * @param \Drupal\Core\Database\Query\SelectInterface $sql_query
    */
   public function __construct(SelectInterface $sql_query) {
     $this->sqlQuery = $sql_query;
+    $this->entityManager = \Drupal::entityManager();
   }
 
   /**
@@ -57,7 +66,6 @@ public function __construct(SelectInterface $sql_query) {
    */
   public function addField($field, $type, $langcode) {
     $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
-    $entity_manager = \Drupal::entityManager();
     $field_info = FieldInfo::fieldInfo();
     $age = $this->sqlQuery->getMetaData('age');
     // This variable ensures grouping works correctly. For example:
@@ -74,7 +82,7 @@ public function addField($field, $type, $langcode) {
     // This will contain the definitions of the last specifier seen by the
     // system.
     $propertyDefinitions = array();
-    $entity_type = $entity_manager->getDefinition($entity_type_id);
+    $entity_type = $this->entityManager->getDefinition($entity_type_id);
     // Use the lightweight and fast field map for checking whether a specifier
     // is a field or not. While calling field_info_field() on every specifier
     // delivers the same information, if no specifiers are using the field API
@@ -141,7 +149,7 @@ public function addField($field, $type, $langcode) {
             if ($bundle_key = $entity_type->getKey('bundle')) {
               $values[$bundle_key] = reset($field_map[$entity_type_id][$field_name]['bundles']);
             }
-            $entity = $entity_manager
+            $entity = $this->entityManager
               ->getStorage($entity_type_id)
               ->create($values);
             $propertyDefinitions = $entity->$field_name->getFieldDefinition()->getPropertyDefinitions();
@@ -174,10 +182,10 @@ public function addField($field, $type, $langcode) {
         $entity_tables = array();
         if ($data_table = $entity_type->getDataTable()) {
           $this->sqlQuery->addMetaData('simple_query', FALSE);
-          $entity_tables[$data_table] = drupal_get_schema($data_table);
+          $entity_tables[$data_table] = $this->getTableMapping($data_table, $entity_type_id);
         }
         $entity_base_table = $entity_type->getBaseTable();
-        $entity_tables[$entity_base_table] = drupal_get_schema($entity_base_table);
+        $entity_tables[$entity_base_table] = $this->getTableMapping($entity_base_table, $entity_type_id);
         $sql_column = $specifier;
         $table = $this->ensureEntityTable($index_prefix, $specifier, $type, $langcode, $base_table, $entity_id_field, $entity_tables);
       }
@@ -195,7 +203,7 @@ public function addField($field, $type, $langcode) {
             $bundles = entity_get_bundles($entity_type_id);
             $values[$bundle_key] = key($bundles);
           }
-          $entity = $entity_manager
+          $entity = $this->entityManager
             ->getStorage($entity_type_id)
             ->create($values);
           $propertyDefinitions = $entity->$specifier->getFieldDefinition()->getPropertyDefinitions();
@@ -206,7 +214,7 @@ public function addField($field, $type, $langcode) {
         if (isset($propertyDefinitions[$relationship_specifier]) && $entity->get($specifier)->first()->get('entity') instanceof EntityReference) {
           // If it is, use the entity type.
           $entity_type_id = $propertyDefinitions[$relationship_specifier]->getTargetDefinition()->getEntityTypeId();
-          $entity_type = $entity_manager->getDefinition($entity_type_id);
+          $entity_type = $this->entityManager->getDefinition($entity_type_id);
           // Add the new entity base table using the table and sql column.
           $join_condition= '%alias.' . $entity_type->getKey('id') . " = $table.$sql_column";
           $base_table = $this->sqlQuery->leftJoin($entity_type->getBaseTable(), NULL, $join_condition);
@@ -230,8 +238,8 @@ public function addField($field, $type, $langcode) {
    * @throws \Drupal\Core\Entity\Query\QueryException
    */
   protected function ensureEntityTable($index_prefix, $property, $type, $langcode, $base_table, $id_field, $entity_tables) {
-    foreach ($entity_tables as $table => $schema) {
-      if (isset($schema['fields'][$property])) {
+    foreach ($entity_tables as $table => $mapping) {
+      if (isset($mapping[$property])) {
         if (!isset($this->entityTables[$index_prefix . $table])) {
           $this->entityTables[$index_prefix . $table] = $this->addJoin($type, $table, "%alias.$id_field = $base_table.$id_field", $langcode);
         }
@@ -272,4 +280,28 @@ protected function addJoin($type, $table, $join_condition, $langcode) {
     return $this->sqlQuery->addJoin($type, $table, NULL, $join_condition, $arguments);
   }
 
+  /**
+   * Returns the schema for the given table.
+   *
+   * @param string $table
+   *   The table name.
+   *
+   * @return array|bool
+   *   The table field mapping for the given table or FALSE if not available.
+   */
+  protected function getTableMapping($table, $entity_type_id) {
+    $storage = $this->entityManager->getStorage($entity_type_id);
+    // @todo Stop calling drupal_get_schema() once menu links are converted
+    //   to the Entity Field API. See https://drupal.org/node/1842858.
+    $schema = drupal_get_schema($table);
+    if (!$schema && $storage instanceof SqlStorageInterface) {
+      $mapping = $storage->getTableMapping();
+      $mapping = isset($mapping[$table]) ? call_user_func_array('array_merge', $mapping[$table]) : FALSE;
+    }
+    else {
+      $mapping = array_keys($schema['fields']);
+    }
+    return array_flip($mapping);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
new file mode 100644
index 0000000..bd96c6c
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
@@ -0,0 +1,474 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\ContentEntitySchemaHandler.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+
+use Drupal\Core\Entity\ContentEntityDatabaseStorageInterface;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\ContentEntityDatabaseStorage;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+
+/**
+ * An entity schema builder that supports revisionable, translatable entities.
+ */
+class ContentEntitySchemaHandler implements ContentEntitySchemaHandlerInterface {
+
+  /**
+   * The entity type this schema builder is responsible for.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityTypeInterface
+   */
+  protected $entityType;
+
+  /**
+   * The base field definitions for this entity type.
+   *
+   * @var \Drupal\Core\Field\FieldDefinitionInterface[]
+   */
+  protected $fieldDefinitions;
+
+  /**
+   * The storage object for the given entity type.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityDatabaseStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * A static cache of the generated schema array.
+   *
+   * @var array
+   */
+  protected $schema;
+
+  /**
+   * 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\ContentEntityDatabaseStorageInterface $storage
+   *   The storage of the entity type. This must be an SQL-based storage.
+   */
+  public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorageInterface $storage) {
+    $this->entityType = $entity_type;
+    $this->fieldDefinitions = $entity_manager->getBaseFieldDefinitions($entity_type->id());
+    $this->storage = $storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSchema() {
+    // Prepare basic information about the entity type.
+    $tables = $this->getTables();
+    // If this entity type does not support storage, no schema information can
+    // be collected.
+    if (empty($tables)) {
+      $this->schema[$this->entityType->id()] = 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();
+      }
+      if (isset($tables['data_table'])) {
+        $schema[$tables['data_table']] = $this->initializeDataTable();
+      }
+      if (isset($tables['revision_data_table'])) {
+        $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable();
+      }
+
+      // Add the schema from field definitions.
+      foreach ($this->storage->getTableMapping() as $table => $field_names) {
+        foreach ($field_names as $field_name => $column_names) {
+          // If there are table fields which do not correspond to entity field
+          // definitions, those are stored with the '' key.
+          if ($field_name !== '') {
+            $this->addFieldSchema($schema[$table], $field_name, $column_names);
+          }
+          else {
+            foreach ($column_names as $column_name) {
+              // Support hardcoded non-entity-field table fields.
+              if ($column_name == 'default_langcode') {
+                $this->addDefaultLangcodeSchema($schema[$table]);
+              }
+            }
+          }
+        }
+      }
+
+      // Process tables after having gathered field information.
+      $this->processBaseTable($schema[$tables['base_table']]);
+      if (isset($tables['revision_table'])) {
+        $this->processRevisionTable($schema[$tables['revision_table']]);
+      }
+      if (isset($tables['data_table'])) {
+        $this->processDataTable($schema[$tables['data_table']]);
+      }
+      if (isset($tables['revision_data_table'])) {
+        $this->processRevisionDataTable($schema[$tables['revision_data_table']]);
+      }
+
+      $this->schema[$this->entityType->id()] = $schema;
+    }
+
+    return $this->schema[$this->entityType->id()];
+  }
+
+  /**
+   * 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(),
+    ));
+  }
+
+  /**
+   * 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.
+   */
+  protected function addFieldSchema(array &$schema, $field_name) {
+    $definition = $this->fieldDefinitions[$field_name];
+    $field_schema = $definition->getSchema();
+    $index = 0;
+
+    foreach ($field_schema['columns'] as $column_name => $column_schema) {
+      // @todo Remove the entity reference check once we can handle schema
+      //   changes or the Entity reference module stops messing with field
+      //   schema. See:
+      //   - https://drupal.org/node/1498720
+      //   - https://drupal.org/node/2209981
+      if ($definition->getType() == 'entity_reference' && $index++ > 0) {
+        continue;
+      }
+      else {
+        $schema_field_name = $this->getFieldColumnName($definition, $column_name);
+      }
+
+      $schema['fields'][$schema_field_name] = $column_schema;
+      $schema['fields'][$schema_field_name]['description'] = $definition->getDescription();
+      // Only entity keys are required.
+      $schema['fields'][$schema_field_name]['not null'] = (bool) $this->entityType->getKey($field_name);
+
+      if (!empty($field_schema['indexes'])) {
+        $indexes = $this->getFieldSchemaData($definition, 'indexes');
+        $schema['indexes'] = !empty($schema['indexes']) ? array_merge($schema['indexes'], $indexes) : $indexes;
+      }
+
+      if (!empty($field_schema['unique keys'])) {
+        $unique_keys = $this->getFieldSchemaData($definition, 'unique keys');
+        $schema['unique keys'] = !empty($schema['unique keys']) ? array_merge($schema['unique keys'], $unique_keys) : $unique_keys;
+      }
+
+      if (!empty($field_schema['foreign keys'])) {
+        $foreign_keys = $this->getFieldForeignKeys($definition);
+        $schema['foreign keys'] = !empty($schema['foreign keys']) ? array_merge($schema['foreign keys'], $foreign_keys) : $foreign_keys;
+      }
+    }
+  }
+
+  /**
+   * Returns field schema data for the given key.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $definition
+   *   The field definition.
+   * @param string $key
+   *   The schema key, e.g. 'indexes'.
+   *
+   * @return array
+   *   The schema definition for the specified key.
+   */
+  protected function getFieldSchemaData(FieldDefinitionInterface $definition, $key) {
+    $data = array();
+    $schema = $definition->getSchema();
+
+    foreach ($schema[$key] as $key => $columns) {
+      $real_name = 'field__' . $this->getFieldColumnName($definition, $key);
+      foreach ($columns as $column) {
+        // Indexes can be specified as either a column name or an array with
+        // column name and length. Allow for either case.
+        $data[$real_name][] = is_array($column) ?
+          array($this->getFieldColumnName($definition, $column[0]), $column[1]) :
+          $this->getFieldColumnName($definition, $column);
+      }
+    }
+
+    return $data;
+  }
+
+  /**
+   * Returns field foreign keys.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $definition
+   *   The field definition.
+   *
+   * @return array
+   *   The schema definition for the foreign keys.
+   */
+  protected function getFieldForeignKeys(FieldDefinitionInterface $definition) {
+    $foreign_keys = array();
+    $schema = $definition->getSchema();
+
+    foreach ($schema['foreign keys'] as $specifier => $specification) {
+      $real_name = $this->getFieldIndexName($definition, $specifier);
+      $foreign_keys[$real_name]['table'] = $specification['table'];
+      foreach ($specification['columns'] as $column => $referenced) {
+        $sql_storage_column = $this->getFieldColumnName($definition, $column);
+        $foreign_keys[$real_name]['columns'][$sql_storage_column] = $referenced;
+      }
+    }
+
+    return $foreign_keys;
+  }
+
+  /**
+   * Returns the name to be used for the given field column.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $definition
+   *   The field definition.
+   *
+   * @return string
+   *   The column name.
+   */
+  public function getFieldColumnName(FieldDefinitionInterface $definition, $column) {
+    $name = $definition->getName();
+    // @todo Remove the entity reference check once we can handle schema
+    //   changes or the Entity reference module stops messing with field
+    //   schema. See:
+    //   - https://drupal.org/node/1498720
+    //   - https://drupal.org/node/2209981
+    return count($definition->getSchema()['columns']) == 1 || $definition->getType() == 'entity_reference' ? $name : $name . '__' . $column;
+  }
+
+  /**
+   * Returns the schema for the 'default_langcode' metadata field.
+   *
+   * @param array $schema
+   *   The table schema to add the field schema to, passed by reference.
+   *
+   * @return array
+   *   A schema field array for the 'default_langcode' metadata field.
+   */
+  protected function addDefaultLangcodeSchema(&$schema) {
+    $schema['fields']['default_langcode'] =  array(
+      'description' => 'Boolean indicating whether field values are in the default entity language.',
+      'type' => 'int',
+      'not null' => TRUE,
+      'default' => 1,
+    );
+  }
+
+  /**
+   * Initializes common information for a base table.
+   *
+   * @return array
+   *   A partial schema array for the base table.
+   */
+  protected function initializeBaseTable() {
+    $schema = array(
+      'description' => "The base table for {$this->entityType->id()} entities.",
+      'primary key' => array($this->entityType->getKey('id')),
+    );
+    if ($key = $this->entityType->getKey('uuid')) {
+      $schema['unique keys'] = array(
+        $this->getEntityIndexName($key) => array($key),
+      );
+    }
+
+    if ($this->storage->getLayoutType() & ContentEntityDatabaseStorage::LAYOUT_REVISION) {
+      $key = $this->entityType->getKey('revision');
+      $schema['unique keys'][$this->getEntityIndexName($key)] = array($key);
+    }
+
+    return $schema;
+  }
+
+  /**
+   * Initializes common information for a revision table.
+   *
+   * @return array
+   *   A partial schema array for the revision table.
+   */
+  protected function initializeRevisionTable() {
+    $id_key = $this->entityType->getKey('id');
+    $revision_key = $this->entityType->getKey('revision');
+
+    $schema = array(
+      'description' => "The revision table for {$this->entityType->id()} entities.",
+      'primary key' => array($revision_key),
+      'foreign keys' => array(
+        $id_key => array(
+          'table' => $this->entityType->getBaseTable(),
+          'columns' => array($id_key => $id_key),
+        ),
+      ),
+    );
+
+    $schema['indexes'][$this->getEntityIndexName($id_key)] = array($id_key);
+
+    $schema['indexes'][$this->getEntityIndexName($revision_key)] = array($revision_key);
+
+    if (isset($this->fieldDefinitions['revision_uid'])) {
+      $schema['indexes'][$this->getEntityIndexName('revision_uid')] = array('revision_uid');
+    }
+
+    return $schema;
+  }
+
+  /**
+   * Initializes common information for a data table.
+   *
+   * @return array
+   *   A partial schema array for the data table.
+   */
+  protected function initializeDataTable() {
+    $id_key = $this->entityType->getKey('id');
+    $schema = array(
+      'description' => "The data table for {$this->entityType->id()} entities.",
+      // @todo Use the language entity key when https://drupal.org/node/2143729
+      //   is in.
+      'primary key' => array($id_key, 'langcode'),
+      'foreign keys' => array(
+        $id_key => array(
+          'table' => $this->entityType->getBaseTable(),
+          'columns' => array($id_key => $id_key),
+        ),
+      ),
+    );
+
+    if ($this->storage->getLayoutType() & ContentEntityDatabaseStorage::LAYOUT_REVISION) {
+      $key = $this->entityType->getKey('revision');
+      $schema['indexes'][$this->getEntityIndexName($key)] = array($key);
+    }
+
+    return $schema;
+  }
+
+  /**
+   * Initializes common information for a revision data table.
+   *
+   * @return array
+   *   A partial schema array for the revision data table.
+   */
+  protected function initializeRevisionDataTable() {
+    $id_key = $this->entityType->getKey('id');
+    $revision_key = $this->entityType->getKey('revision');
+
+    $schema = array(
+      'description' => "The revision data table for {$this->entityType->id()} entities.",
+      // @todo Use the language entity key when https://drupal.org/node/2143729
+      //   is in.
+      'primary key' => array($revision_key, 'langcode'),
+      'foreign keys' => array(
+        $id_key => array(
+          'table' => $this->entityType->getBaseTable(),
+          'columns' => array($id_key => $id_key),
+        ),
+        $revision_key => array(
+          'table' => $this->entityType->getRevisionTable(),
+          'columns' => array($revision_key => $revision_key),
+        )
+      ),
+    );
+
+    $schema['indexes'][$this->getEntityIndexName($id_key)] = array($id_key);
+
+    return $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.
+   *
+   * @param array $schema
+   *   The table schema, passed by reference.
+   *
+   * @return array
+   *   A partial schema array for the base table.
+   */
+  protected function processDataTable(array &$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 processRevisionDataTable(array &$schema) {
+  }
+
+  /**
+   * Processes the specified entity key.
+   *
+   * @param string $key
+   *   The entity key name.
+   */
+  protected function processIdentifierSchema(&$schema, $key) {
+    if ($schema['fields'][$key]['type'] == 'int') {
+      $schema['fields'][$key]['type'] = 'serial';
+    }
+    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..5b958d8
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\ContentEntitySchemaHandlerInterface.
+ */
+namespace Drupal\Core\Entity\Schema;
+
+/**
+ * A common interface for building the database schema for entities.
+ */
+interface ContentEntitySchemaHandlerInterface {
+
+  /**
+   * 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/SqlStorageInterface.php b/core/lib/Drupal/Core/Entity/SqlStorageInterface.php
new file mode 100644
index 0000000..4bff3c2
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/SqlStorageInterface.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\SqlStorageInterface.
+ */
+
+namespace Drupal\Core\Entity;
+
+/**
+ * A common interface for SQL-based storage controllers.
+ */
+interface SqlStorageInterface {
+
+  /**
+   * Gets a mapping of schema fields that will be stored per entity table.
+   *
+   * @return array
+   *  An nested array where the keys are the names of the entity tables and the
+   *  values are arrays containing the names of the fields that will be stored
+   *  in the respective tables.
+   */
+  public function getTableMapping();
+
+}
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
index 4ad8849..6560de1 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Field\Plugin\Field\FieldType;
 
+use Drupal\Component\Utility\String;
 use Drupal\Core\Entity\TypedData\EntityDataDefinition;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldItemBase;
@@ -58,6 +59,10 @@ public static function propertyDefinitions(FieldDefinitionInterface $field_defin
     $settings = $field_definition->getSettings();
     $target_type_info = \Drupal::entityManager()->getDefinition($settings['target_type']);
 
+    if (!$target_type_info) {
+      throw new \Exception(String::format('Target type @entity_type does not exist.', array('@target_type' => $settings['target_type'])));
+    }
+
     if ($target_type_info->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface')) {
       // @todo: Lookup the entity type's ID data type and use it here.
       // https://drupal.org/node/2107249
@@ -98,6 +103,9 @@ public static function mainPropertyName() {
   public static function schema(FieldDefinitionInterface $field_definition) {
     $target_type = $field_definition->getSetting('target_type');
     $target_type_info = \Drupal::entityManager()->getDefinition($target_type);
+    if (!$target_type_info) {
+      throw new \Exception(String::format('Target type @entity_type does not exist.', array('@entity_type' => $target_type)));
+    }
 
     if ($target_type_info->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface')) {
       $columns = array(
diff --git a/core/modules/aggregator/aggregator.install b/core/modules/aggregator/aggregator.install
index e4bef0e..2b755e1 100644
--- a/core/modules/aggregator/aggregator.install
+++ b/core/modules/aggregator/aggregator.install
@@ -22,179 +22,3 @@ function aggregator_requirements($phase) {
   }
   return $requirements;
 }
-
-/**
- * Implements hook_schema().
- */
-function aggregator_schema() {
-  $schema['aggregator_feed'] = array(
-    'description' => 'Stores feeds to be parsed by the aggregator.',
-    'fields' => array(
-      'fid' => array(
-        'type' => 'serial',
-        'not null' => TRUE,
-        'description' => 'Primary Key: Unique feed ID.',
-      ),
-      'uuid' => array(
-        'description' => 'Unique Key: Universally unique identifier for this entity.',
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => FALSE,
-      ),
-      'title' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'Title of the feed.',
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of this feed.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'url' => array(
-        'type' => 'text',
-        'not null' => TRUE,
-        'description' => 'URL to the feed.',
-      ),
-      'refresh' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'How often to check for new feed items, in seconds.',
-      ),
-      'checked' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'Last time feed was checked for new items, as Unix timestamp.',
-      ),
-      'queued' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'Time when this feed was queued for refresh, 0 if not queued.',
-      ),
-      'link' => array(
-        'type' => 'text',
-        'not null' => TRUE,
-        'description' => 'The parent website of the feed; comes from the <link> element in the feed.',
-      ),
-      'description' => array(
-        'type' => 'text',
-        'not null' => TRUE,
-        'size' => 'big',
-        'description' => "The parent website's description; comes from the <description> element in the feed.",
-      ),
-      'image' => array(
-        'type' => 'text',
-        'not null' => TRUE,
-        'size' => 'big',
-        'description' => 'An image representing the feed.',
-      ),
-      'hash' => array(
-        'type' => 'varchar',
-        'length' => 64,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'Calculated hash of the feed data, used for validating cache.',
-      ),
-      'etag' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'Entity tag HTTP response header, used for validating cache.',
-      ),
-      'modified' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'When the feed was last modified, as a Unix timestamp.',
-      ),
-    ),
-    'primary key' => array('fid'),
-    'indexes' => array(
-      'url'  => array(array('url', 255)),
-      'queued' => array('queued'),
-    ),
-    'unique keys' => array(
-      'title' => array('title'),
-    ),
-  );
-
-  $schema['aggregator_item'] = array(
-    'description' => 'Stores the individual items imported from feeds.',
-    'fields' => array(
-      'iid'  => array(
-        'type' => 'serial',
-        'not null' => TRUE,
-        'description' => 'Primary Key: Unique ID for feed item.',
-      ),
-      'fid' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'The {aggregator_feed}.fid to which this item belongs.',
-      ),
-      'title' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'Title of the feed item.',
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of this feed item.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'link' => array(
-        'type' => 'text',
-        'not null' => TRUE,
-        'description' => 'Link to the feed item.',
-      ),
-      'author' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'Author of the feed item.',
-      ),
-      'description' => array(
-        'type' => 'text',
-        'not null' => TRUE,
-        'size' => 'big',
-        'description' => 'Body of the feed item.',
-      ),
-      'timestamp' => array(
-        'type' => 'int',
-        'not null' => FALSE,
-        'description' => 'Posted date of the feed item, as a Unix timestamp.',
-      ),
-      'guid' => array(
-        'type' => 'text',
-        'not null' => TRUE,
-        'description' => 'Unique identifier for the feed item.',
-      )
-    ),
-    'primary key' => array('iid'),
-    'indexes' => array(
-      'fid' => array('fid'),
-      'timestamp' => array('timestamp'),
-    ),
-    'foreign keys' => array(
-      'aggregator_feed' => array(
-        'table' => 'aggregator_feed',
-        'columns' => array('fid' => 'fid'),
-      ),
-    ),
-  );
-
-  return $schema;
-}
diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Entity/Feed.php b/core/modules/aggregator/lib/Drupal/aggregator/Entity/Feed.php
index daa3fba..4093037 100644
--- a/core/modules/aggregator/lib/Drupal/aggregator/Entity/Feed.php
+++ b/core/modules/aggregator/lib/Drupal/aggregator/Entity/Feed.php
@@ -183,11 +183,13 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
 
     $fields['checked'] = FieldDefinition::create('timestamp')
       ->setLabel(t('Checked'))
-      ->setDescription(t('Last time feed was checked for new items, as Unix timestamp.'));
+      ->setDescription(t('Last time feed was checked for new items, as Unix timestamp.'))
+      ->setSetting('default_value', 0);
 
     $fields['queued'] = FieldDefinition::create('timestamp')
       ->setLabel(t('Queued'))
-      ->setDescription(t('Time when this feed was queued for refresh, 0 if not queued.'));
+      ->setDescription(t('Time when this feed was queued for refresh, 0 if not queued.'))
+      ->setSetting('default_value', 0);
 
     $fields['link'] = FieldDefinition::create('uri')
       ->setLabel(t('Link'))
diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Entity/Item.php b/core/modules/aggregator/lib/Drupal/aggregator/Entity/Item.php
index f12ed40..804d42a 100644
--- a/core/modules/aggregator/lib/Drupal/aggregator/Entity/Item.php
+++ b/core/modules/aggregator/lib/Drupal/aggregator/Entity/Item.php
@@ -78,8 +78,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
       ->setLabel(t('Author'))
       ->setDescription(t('The author of the feed item.'));
 
-    // @todo Convert to a text field in https://drupal.org/node/2149845.
-    $fields['description'] = FieldDefinition::create('string')
+    $fields['description'] = FieldDefinition::create('string_long')
       ->setLabel(t('Description'))
       ->setDescription(t('The body of the feed item.'));
 
@@ -88,7 +87,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
       ->setDescription(t('Posted date of the feed item, as a Unix timestamp.'));
 
     // @todo Convert to a real UUID field in https://drupal.org/node/2149851.
-    $fields['guid'] = FieldDefinition::create('string')
+    $fields['guid'] = FieldDefinition::create('string_long')
       ->setLabel(t('GUID'))
       ->setDescription(t('Unique identifier for the feed item.'));
 
diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Tests/Views/IntegrationTest.php b/core/modules/aggregator/lib/Drupal/aggregator/Tests/Views/IntegrationTest.php
index 3f4d7b5..e26f889 100644
--- a/core/modules/aggregator/lib/Drupal/aggregator/Tests/Views/IntegrationTest.php
+++ b/core/modules/aggregator/lib/Drupal/aggregator/Tests/Views/IntegrationTest.php
@@ -55,7 +55,8 @@ public static function getInfo() {
   protected function setUp() {
     parent::setUp();
 
-    $this->installSchema('aggregator', array('aggregator_item', 'aggregator_feed'));
+    $this->installEntitySchema('aggregator_item');
+    $this->installEntitySchema('aggregator_feed');
 
     ViewTestData::createTestViews(get_class($this), array('aggregator_test_views'));
 
diff --git a/core/modules/block/custom_block/custom_block.install b/core/modules/block/custom_block/custom_block.install
index 6c27dd9..f7042b3 100644
--- a/core/modules/block/custom_block/custom_block.install
+++ b/core/modules/block/custom_block/custom_block.install
@@ -8,123 +8,6 @@
 use Drupal\Core\Entity\EntityTypeInterface;
 
 /**
- * Implements hook_schema().
- */
-function custom_block_schema() {
-  $schema = array();
-  $schema['custom_block'] = array(
-    'description' => 'Stores contents of custom-made blocks.',
-    'fields' => array(
-      'id' => array(
-        'type' => 'serial',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => "The block's {custom_block}.id.",
-      ),
-      'uuid' => array(
-        'description' => 'Unique Key: Universally unique identifier for this entity.',
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => FALSE,
-      ),
-      'info' => array(
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'Block description.',
-      ),
-      // Defaults to NULL in order to avoid a brief period of potential
-      // deadlocks on the index.
-      'revision_id' => array(
-        'description' => 'The current {block_custom_revision}.revision_id version identifier.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => FALSE,
-        'default' => NULL,
-      ),
-      'type' => array(
-        'description' => 'The type of this custom block.',
-        'type' => 'varchar',
-        'length' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'changed' => array(
-        'description' => 'The Unix timestamp when the custom block was most recently saved.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of this node.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-    ),
-    'primary key' => array('id'),
-    'indexes' => array(
-      'block_custom_type' => array('type'),
-    ),
-    'unique keys' => array(
-      'revision_id' => array('revision_id'),
-      'uuid' => array('uuid'),
-      'info' => array('info'),
-    ),
-    'foreign keys' => array(
-      'custom_block_revision' => array(
-        'table' => 'custom_block_revision',
-        'columns' => array('revision_id' => 'revision_id'),
-      ),
-    ),
-  );
-
-  $schema['custom_block_revision'] = array(
-    'description' => 'Stores contents of custom-made blocks.',
-    'fields' => array(
-      'id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => "The block's {custom_block}.id.",
-      ),
-      // Defaults to NULL in order to avoid a brief period of potential
-      // deadlocks on the index.
-      'revision_id' => array(
-        'description' => 'The current version identifier.',
-        'type' => 'serial',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-      ),
-      'log' => array(
-        'description' => 'The log entry explaining the changes in this version.',
-        'type' => 'text',
-        'not null' => TRUE,
-        'size' => 'big',
-      ),
-      'info' => array(
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'Block description.',
-      ),
-      'changed' => array(
-        'description' => 'The Unix timestamp when the version was most recently saved.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-    ),
-    'primary key' => array('revision_id'),
-  );
-  return $schema;
-}
-
-/**
  * Implements hook_schema_0().
  */
 function custom_block_schema_0() {
diff --git a/core/modules/comment/comment.install b/core/modules/comment/comment.install
index 40fb1a0..1929b7b 100644
--- a/core/modules/comment/comment.install
+++ b/core/modules/comment/comment.install
@@ -28,229 +28,3 @@ function comment_install() {
   // @see \Drupal\comment\CommentStorage::updateEntityStatistics().
   \Drupal::state()->set('comment.maintain_entity_statistics', TRUE);
 }
-
-/**
- * Implements hook_schema().
- */
-function comment_schema() {
-  $schema['comment'] = array(
-    'description' => 'Stores comments and associated data.',
-    'fields' => array(
-      'cid' => array(
-        'type' => 'serial',
-        'not null' => TRUE,
-        'description' => 'Primary Key: Unique comment ID.',
-      ),
-      'uuid' => array(
-        'description' => 'Unique Key: Universally unique identifier for this entity.',
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => FALSE,
-      ),
-      'pid' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'The {comment}.cid to which this comment is a reply. If set to 0, this comment is not a reply to an existing comment.',
-      ),
-      'entity_id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'The entity_id of the entity to which this comment is a reply.',
-      ),
-      'entity_type' => array(
-        'type' => 'varchar',
-        'not null' => TRUE,
-        'default' => 'node',
-        'length' => 255,
-        'description' => 'The entity_type of the entity to which this comment is a reply.',
-      ),
-      'field_id' => array(
-        'type' => 'varchar',
-        'not null' => TRUE,
-        'default' => 'node.comment',
-        'length' => 255,
-        'description' => 'The field_id of the field that was used to add this comment.',
-      ),
-      'uid' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'The {users}.uid who authored the comment. If set to 0, this comment was created by an anonymous user.',
-      ),
-      'subject' => array(
-        'type' => 'varchar',
-        'length' => 64,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'The comment title.',
-      ),
-      'hostname' => array(
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => "The author's host name.",
-      ),
-      'created' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'The time that the comment was created, as a Unix timestamp.',
-      ),
-      'changed' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'The time that the comment was last edited, as a Unix timestamp.',
-      ),
-      'status' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 1,
-        'size' => 'tiny',
-        'description' => 'The published status of a comment. (0 = Not Published, 1 = Published)',
-      ),
-      'thread' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'description' => "The alphadecimal representation of the comment's place in a thread, consisting of a base 36 string prefixed by an integer indicating its length.",
-      ),
-      'name' => array(
-        'type' => 'varchar',
-        'length' => 60,
-        'not null' => FALSE,
-        'description' => "The comment author's name. Uses {users}.name if the user is logged in, otherwise uses the value typed into the comment form.",
-      ),
-      'mail' => array(
-        'type' => 'varchar',
-        'length' => 64,
-        'not null' => FALSE,
-        'description' => "The comment author's e-mail address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on.",
-      ),
-      'homepage' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => FALSE,
-        'description' => "The comment author's home page address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on.",
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of this comment.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-    ),
-    'indexes' => array(
-      'comment_status_pid' => array('pid', 'status'),
-      'comment_num_new' => array(
-        'entity_id',
-        array('entity_type', 32),
-        array('field_id', 32),
-        'status',
-        'created',
-        'cid',
-        'thread',
-      ),
-      'comment_uid' => array('uid'),
-      'comment_entity_langcode' => array(
-        'entity_id',
-        array('entity_type', 32),
-        array('field_id', 32),
-        'langcode',
-      ),
-      'comment_created' => array('created'),
-    ),
-    'primary key' => array('cid'),
-    'unique keys' => array(
-      'uuid' => array('uuid'),
-    ),
-    'foreign keys' => array(
-      'comment_author' => array(
-        'table' => 'users',
-        'columns' => array('uid' => 'uid'),
-      ),
-    ),
-  );
-
-  $schema['comment_entity_statistics'] = array(
-    'description' => 'Maintains statistics of entity and comments posts to show "new" and "updated" flags.',
-    'fields' => array(
-      'entity_id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'The entity_id of the entity for which the statistics are compiled.',
-      ),
-      'entity_type' => array(
-        'type' => 'varchar',
-        'not null' => TRUE,
-        'default' => 'node',
-        'length' => 255,
-        'description' => 'The entity_type of the entity to which this comment is a reply.',
-      ),
-      'field_id' => array(
-        'type' => 'varchar',
-        'not null' => TRUE,
-        'default' => 'node__comment',
-        'length' => 255,
-        'description' => 'The field_id of the field that was used to add this comment.',
-      ),
-      'cid' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'The {comment}.cid of the last comment.',
-      ),
-      'last_comment_timestamp' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.changed.',
-      ),
-      'last_comment_name' => array(
-        'type' => 'varchar',
-        'length' => 60,
-        'not null' => FALSE,
-        'description' => 'The name of the latest author to post a comment on this node, from {comment}.name.',
-      ),
-      'last_comment_uid' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'The user ID of the latest author to post a comment on this node, from {comment}.uid.',
-      ),
-      'comment_count' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'The total number of comments on this entity.',
-      ),
-    ),
-    'primary key' => array('entity_id', array('entity_type', 32), array('field_id', 32)),
-    'indexes' => array(
-      'last_comment_timestamp' => array('last_comment_timestamp'),
-      'comment_count' => array('comment_count'),
-      'last_comment_uid' => array('last_comment_uid'),
-    ),
-    'foreign keys' => array(
-      'last_comment_author' => array(
-        'table' => 'users',
-        'columns' => array(
-          'last_comment_uid' => 'uid',
-        ),
-      ),
-    ),
-  );
-
-  return $schema;
-}
diff --git a/core/modules/comment/lib/Drupal/comment/CommentStorage.php b/core/modules/comment/lib/Drupal/comment/CommentStorage.php
index d05b2c3..02a411f 100644
--- a/core/modules/comment/lib/Drupal/comment/CommentStorage.php
+++ b/core/modules/comment/lib/Drupal/comment/CommentStorage.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 use Drupal\field\FieldInfo;
@@ -36,13 +37,15 @@ class CommentStorage extends ContentEntityDatabaseStorage implements CommentStor
    *   An array of entity info for the entity type.
    * @param \Drupal\Core\Database\Connection $database
    *   The database connection to be used.
-   * @param \Drupal\field\FieldInfo $field_info
-   *   The field info service.
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
    * @param \Drupal\comment\CommentStatisticsInterface $comment_statistics
    *   The comment statistics service.
+   * @param \Drupal\field\FieldInfo $field_info
+   *   The field info service.
    */
-  public function __construct(EntityTypeInterface $entity_info, Connection $database, FieldInfo $field_info, CommentStatisticsInterface $comment_statistics) {
-    parent::__construct($entity_info, $database, $field_info);
+  public function __construct(EntityTypeInterface $entity_info, Connection $database, EntityManagerInterface $entity_manager, CommentStatisticsInterface $comment_statistics, FieldInfo $field_info) {
+    parent::__construct($entity_info, $database, $entity_manager, $field_info);
     $this->statistics = $comment_statistics;
   }
 
@@ -53,8 +56,9 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
     return new static(
       $entity_info,
       $container->get('database'),
-      $container->get('field.info'),
-      $container->get('comment.statistics')
+      $container->get('entity.manager'),
+      $container->get('comment.statistics'),
+      $container->get('field.info')
     );
   }
 
@@ -127,4 +131,86 @@ public function getChildCids(array $comments) {
       ->fetchCol();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildSchema() {
+    $schema = parent::buildSchema();
+
+    $schema['comment_entity_statistics'] = array(
+      'description' => 'Maintains statistics of entity and comments posts to show "new" and "updated" flags.',
+      'fields' => array(
+        'entity_id' => array(
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+          'description' => 'The entity_id of the entity for which the statistics are compiled.',
+        ),
+        'entity_type' => array(
+          'type' => 'varchar',
+          'not null' => TRUE,
+          'default' => 'node',
+          'length' => 255,
+          'description' => 'The entity_type of the entity to which this comment is a reply.',
+        ),
+        'field_id' => array(
+          'type' => 'varchar',
+          'not null' => TRUE,
+          'default' => 'node__comment',
+          'length' => 255,
+          'description' => 'The field_id of the field that was used to add this comment.',
+        ),
+        'cid' => array(
+          'type' => 'int',
+          'not null' => TRUE,
+          'default' => 0,
+          'description' => 'The {comment}.cid of the last comment.',
+        ),
+        'last_comment_timestamp' => array(
+          'type' => 'int',
+          'not null' => TRUE,
+          'default' => 0,
+          'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.changed.',
+        ),
+        'last_comment_name' => array(
+          'type' => 'varchar',
+          'length' => 60,
+          'not null' => FALSE,
+          'description' => 'The name of the latest author to post a comment on this node, from {comment}.name.',
+        ),
+        'last_comment_uid' => array(
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+          'description' => 'The user ID of the latest author to post a comment on this node, from {comment}.uid.',
+        ),
+        'comment_count' => array(
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+          'description' => 'The total number of comments on this entity.',
+        ),
+      ),
+      'primary key' => array('entity_id', array('entity_type', 32), array('field_id', 32)),
+      'indexes' => array(
+        'last_comment_timestamp' => array('last_comment_timestamp'),
+        'comment_count' => array('comment_count'),
+        'last_comment_uid' => array('last_comment_uid'),
+      ),
+      'foreign keys' => array(
+        'last_comment_author' => array(
+          'table' => 'users',
+          'columns' => array(
+            'last_comment_uid' => 'uid',
+          ),
+        ),
+      ),
+    );
+
+    return $schema;
+  }
+
 }
diff --git a/core/modules/comment/lib/Drupal/comment/Entity/Comment.php b/core/modules/comment/lib/Drupal/comment/Entity/Comment.php
index 6ec3e82..1dbfc76 100644
--- a/core/modules/comment/lib/Drupal/comment/Entity/Comment.php
+++ b/core/modules/comment/lib/Drupal/comment/Entity/Comment.php
@@ -280,7 +280,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
 
     $fields['status'] = FieldDefinition::create('boolean')
       ->setLabel(t('Publishing status'))
-      ->setDescription(t('A boolean indicating whether the comment is published.'));
+      ->setDescription(t('A boolean indicating whether the comment is published.'))
+      ->setSetting('default_value', TRUE);
 
     $fields['thread'] = FieldDefinition::create('string')
       ->setLabel(t('Thread place'))
diff --git a/core/modules/comment/lib/Drupal/comment/Tests/CommentValidationTest.php b/core/modules/comment/lib/Drupal/comment/Tests/CommentValidationTest.php
index 881c965..ae613db 100644
--- a/core/modules/comment/lib/Drupal/comment/Tests/CommentValidationTest.php
+++ b/core/modules/comment/lib/Drupal/comment/Tests/CommentValidationTest.php
@@ -38,7 +38,7 @@ public static function getInfo() {
    */
   public function setUp() {
     parent::setUp();
-    $this->installSchema('node', array('node', 'node_field_data', 'node_field_revision', 'node_revision'));
+    $this->installEntitySchema('node');
   }
 
   /**
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php
index 3f86c9b..4f4d5ca 100644
--- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php
@@ -29,7 +29,7 @@ class ConfigImportRecreateTest extends DrupalUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('system', 'entity', 'field', 'text', 'node');
+  public static $modules = array('system', 'entity', 'field', 'text', 'node', 'user');
 
   public static function getInfo() {
     return array(
@@ -43,7 +43,7 @@ public function setUp() {
     parent::setUp();
 
     $this->installSchema('system', 'config_snapshot');
-    $this->installSchema('node', 'node');
+    $this->installEntitySchema('node');
 
     $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
 
diff --git a/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php b/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php
index 753745f..0ae29c8 100644
--- a/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php
+++ b/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php
@@ -27,7 +27,7 @@ class EditTestBase extends DrupalUnitTestBase {
   protected function setUp() {
     parent::setUp();
 
-    $this->installSchema('entity_test', array('entity_test', 'entity_test_rev'));
+    $this->installEntitySchema('entity_test');
     $this->installConfig(array('field', 'filter'));
   }
 
diff --git a/core/modules/editor/lib/Drupal/editor/Tests/EditorFileUsageTest.php b/core/modules/editor/lib/Drupal/editor/Tests/EditorFileUsageTest.php
index 3be50ec..40af4bc 100644
--- a/core/modules/editor/lib/Drupal/editor/Tests/EditorFileUsageTest.php
+++ b/core/modules/editor/lib/Drupal/editor/Tests/EditorFileUsageTest.php
@@ -31,8 +31,8 @@ public static function getInfo() {
 
   public function setUp() {
     parent::setUp();
-    $this->installSchema('node', array('node', 'node_access', 'node_field_data', 'node_field_revision', 'node_revision'));
-    $this->installSchema('file', array('file_managed', 'file_usage'));
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('file');
 
     // Add text formats.
     $filtered_html_format = entity_create('filter_format', array(
diff --git a/core/modules/entity/entity.module b/core/modules/entity/entity.module
index 1fa3852..7bbc291 100644
--- a/core/modules/entity/entity.module
+++ b/core/modules/entity/entity.module
@@ -9,6 +9,7 @@
  */
 
 use Drupal\Core\Config\Entity\ConfigEntityStorage;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandlerInterface;
 
 /**
  * Implements hook_help().
@@ -99,6 +100,38 @@ function entity_entity_bundle_delete($entity_type_id, $bundle) {
 }
 
 /**
+ * Implements hook_modules_installed().
+ */
+function entity_modules_installed($modules) {
+  // Install entity type tables.
+  $entity_manager = \Drupal::entityManager();
+  $schema = \Drupal::database()->schema();
+  $definitions = $entity_manager->getDefinitions();
+  $installed = array();
+
+  foreach ($modules as $module) {
+    foreach ($definitions as $entity_type_id => $entity_type) {
+      if ($entity_type->getProvider() == $module) {
+        $storage = $entity_manager->getStorage($entity_type_id);
+        if ($storage instanceof ContentEntitySchemaHandlerInterface) {
+          foreach ($storage->getSchema() as $table_name => $table_schema) {
+            // @todo Remove the drupal_get_schema() call once all entity types
+            //   have been converted to an automatic schema.
+            if (!drupal_get_schema($table_name) && !$schema->tableExists($table_name)) {
+              $schema->createTable($table_name, $table_schema);
+              $installed[$entity_type_id] = $storage;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  // TODO Document this hook.
+  \Drupal::moduleHandler()->invokeAll('entity_schema_installed', array($installed));
+}
+
+/**
  * Implements hook_module_preuninstall().
  */
 function entity_module_preuninstall($module) {
@@ -112,3 +145,30 @@ function entity_module_preuninstall($module) {
     }
   }
 }
+
+/**
+ * Implements hook_modules_uninstalled().
+ */
+function entity_modules_uninstalled($modules) {
+  $entity_manager = \Drupal::entityManager();
+  $schema = \Drupal::database()->schema();
+  $definitions = $entity_manager->getDefinitions();
+
+  foreach ($modules as $module) {
+    foreach ($definitions as $entity_type_id => $entity_type) {
+      if ($entity_type->getProvider() == $module) {
+        // Remove entity tables.
+        $storage = $entity_manager->getStorage($entity_type->id());
+        if ($storage instanceof ContentEntitySchemaHandlerInterface) {
+          foreach ($storage->getSchema() as $table_name => $table_schema) {
+            // @todo Remove the drupal_get_schema() call once all entity types
+            //   have been converted to an automatic schema.
+            if (!drupal_get_schema($table_name) && $schema->tableExists($table_name)) {
+              $schema->dropTable($table_name, $table_schema);
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/core/modules/entity/lib/Drupal/entity/Tests/EntityDisplayTest.php b/core/modules/entity/lib/Drupal/entity/Tests/EntityDisplayTest.php
index 31ea4e8..de443b4 100644
--- a/core/modules/entity/lib/Drupal/entity/Tests/EntityDisplayTest.php
+++ b/core/modules/entity/lib/Drupal/entity/Tests/EntityDisplayTest.php
@@ -261,7 +261,7 @@ public function testBaseFieldComponent() {
    */
   public function testRenameDeleteBundle() {
     $this->enableModules(array('field_test', 'node', 'system', 'text'));
-    $this->installSchema('node', array('node'));
+    $this->installEntitySchema('node');
 
     // Create a node bundle, display and form display object.
     entity_create('node_type', array('type' => 'article'))->save();
diff --git a/core/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceFieldTest.php b/core/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceFieldTest.php
index f035f37..f032080 100644
--- a/core/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceFieldTest.php
+++ b/core/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceFieldTest.php
@@ -75,7 +75,7 @@ public static function getInfo() {
   public function setUp() {
     parent::setUp();
 
-    $this->installSchema('entity_test', array('entity_test_rev', 'entity_test_rev_revision'));
+    $this->installEntitySchema('entity_test_rev');
 
     // Setup a field and instance.
     entity_reference_create_instance(
diff --git a/core/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceItemTest.php b/core/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceItemTest.php
index 8c33d47..0586e7c 100644
--- a/core/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceItemTest.php
+++ b/core/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceItemTest.php
@@ -52,8 +52,7 @@ public static function getInfo() {
   public function setUp() {
     parent::setUp();
 
-    $this->installSchema('taxonomy', 'taxonomy_term_data');
-    $this->installSchema('taxonomy', 'taxonomy_term_hierarchy');
+    $this->installEntitySchema('taxonomy_term');
 
     $this->vocabulary = entity_create('taxonomy_vocabulary', array(
       'name' => $this->randomName(),
diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldAttachOtherTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldAttachOtherTest.php
index 9e42126..7c46cd8 100644
--- a/core/modules/field/lib/Drupal/field/Tests/FieldAttachOtherTest.php
+++ b/core/modules/field/lib/Drupal/field/Tests/FieldAttachOtherTest.php
@@ -38,7 +38,7 @@ public static function getInfo() {
 
   public function setUp() {
     parent::setUp();
-    $this->installSchema('entity_test', array('entity_test_rev', 'entity_test_rev_revision'));
+    $this->installEntitySchema('entity_test_rev');
     $this->createFieldWithInstance();
   }
 
diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php
index 4b0e85e..eed2791 100644
--- a/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php
+++ b/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php
@@ -36,7 +36,7 @@ public static function getInfo() {
 
   public function setUp() {
     parent::setUp();
-    $this->installSchema('entity_test', array('entity_test_rev', 'entity_test_rev_revision'));
+    $this->installEntitySchema('entity_test_rev');
   }
 
   /**
diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldInfoTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldInfoTest.php
index 4999a83..5c9c32d 100644
--- a/core/modules/field/lib/Drupal/field/Tests/FieldInfoTest.php
+++ b/core/modules/field/lib/Drupal/field/Tests/FieldInfoTest.php
@@ -177,7 +177,6 @@ function testInstancePrepare() {
   function testInstanceDisabledEntityType() {
     // Disabling a module invokes user_modules_uninstalled() and calls
     // drupal_flush_all_caches(). Install the necessary schema to support this.
-    $this->installSchema('user', array('users_data'));
     $this->installSchema('system', array('router'));
 
     // For this test the field type and the entity type must be exposed by
diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php b/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php
index 5f77d92..be551be 100644
--- a/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php
+++ b/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php
@@ -36,9 +36,9 @@
    */
   function setUp() {
     parent::setUp();
-    $this->installSchema('entity_test', 'entity_test');
+    $this->installEntitySchema('entity_test');
+    $this->installEntitySchema('user');
     $this->installSchema('system', array('sequences', 'config_snapshot'));
-    $this->installSchema('user', array('users', 'users_roles'));
 
     // Set default storage backend and configure the theme system.
     $this->installConfig(array('field', 'system'));
diff --git a/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php b/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php
index ba00c74..fb52e72 100644
--- a/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php
+++ b/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php
@@ -33,7 +33,7 @@ class TranslationWebTest extends FieldTestBase {
    *
    * @var string
    */
-  protected $entity_type = 'entity_test_rev';
+  protected $entity_type = 'entity_test_mulrev';
 
   /**
    * The field to use in this test.
@@ -78,7 +78,7 @@ function setUp() {
       'bundle' => $this->entity_type,
     );
     entity_create('field_instance_config', $instance)->save();
-    $this->instance = entity_load('field_instance_config', 'entity_test.' . $instance['bundle'] . '.' . $this->field_name);
+    $this->instance = entity_load('field_instance_config', $this->entity_type . '.' . $instance['bundle'] . '.' . $this->field_name);
 
     entity_get_form_display($this->entity_type, $this->entity_type, 'default')
       ->setComponent($this->field_name)
diff --git a/core/modules/file/file.install b/core/modules/file/file.install
index fb5a4eb..6dc06a2 100644
--- a/core/modules/file/file.install
+++ b/core/modules/file/file.install
@@ -6,157 +6,6 @@
  */
 
 /**
- * Implements hook_schema().
- */
-function file_schema() {
-  $schema['file_managed'] = array(
-    'description' => 'Stores information for uploaded files.',
-    'fields' => array(
-      'fid' => array(
-        'description' => 'File ID.',
-        'type' => 'serial',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-      ),
-      'uuid' => array(
-        'description' => 'Unique Key: Universally unique identifier for this entity.',
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => FALSE,
-      ),
-      'uid' => array(
-        'description' => 'The {users}.uid of the user who is associated with the file.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'filename' => array(
-        'description' => 'Name of the file with no path components. This may differ from the basename of the URI if the file is renamed to avoid overwriting an existing file.',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'uri' => array(
-        'description' => 'The URI to access the file (either local or remote).',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'binary' => TRUE,
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of this file.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'filemime' => array(
-        'description' => "The file's MIME type.",
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'filesize' => array(
-        'description' => 'The size of the file in bytes.',
-        'type' => 'int',
-        'size' => 'big',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'status' => array(
-        'description' => 'A field indicating the status of the file. Two status are defined in core: temporary (0) and permanent (1). Temporary files older than DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed during a cron run.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'size' => 'tiny',
-      ),
-      'created' => array(
-        'description' => 'UNIX timestamp for when the file added.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'changed' => array(
-        'description' => 'UNIX timestamp for when the file was last changed.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-    ),
-    'indexes' => array(
-      'uid' => array('uid'),
-      'status' => array('status'),
-      'changed' => array('changed'),
-    ),
-    'unique keys' => array(
-      'uuid' => array('uuid'),
-      'uri' => array('uri'),
-    ),
-    'primary key' => array('fid'),
-    'foreign keys' => array(
-      'file_owner' => array(
-        'table' => 'users',
-        'columns' => array('uid' => 'uid'),
-      ),
-    ),
-  );
-
-  $schema['file_usage'] = array(
-    'description' => 'Track where a file is used.',
-    'fields' => array(
-      'fid' => array(
-        'description' => 'File ID.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-      ),
-      'module' => array(
-        'description' => 'The name of the module that is using the file.',
-        'type' => 'varchar',
-        'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'type' => array(
-        'description' => 'The name of the object type in which the file is used.',
-        'type' => 'varchar',
-        'length' => 64,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'id' => array(
-        'description' => 'The primary key of the object using the file.',
-        'type' => 'varchar',
-        'length' => 64,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'count' => array(
-        'description' => 'The number of times this file is used by this object.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-    ),
-    'primary key' => array('fid', 'type', 'id', 'module'),
-    'indexes' => array(
-      'type_id' => array('type', 'id'),
-      'fid_count' => array('fid', 'count'),
-      'fid_module' => array('fid', 'module'),
-    ),
-  );
-  return $schema;
-}
-
-/**
  * Implements hook_requirements().
  *
  * Display information about getting upload progress bars working.
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index b707b57..52a689d 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -254,6 +254,7 @@ function file_move(File $source, $destination = NULL, $replace = FILE_EXISTS_REN
         $existing = reset($existing_files);
         $delete_source = TRUE;
         $file->fid = $existing->id();
+        $file->uuid = $existing->uuid();
       }
     }
     // If we are renaming around an existing file (rather than a directory),
diff --git a/core/modules/file/lib/Drupal/file/FileStorage.php b/core/modules/file/lib/Drupal/file/FileStorage.php
index c6f2ccc..f8fc4d7 100644
--- a/core/modules/file/lib/Drupal/file/FileStorage.php
+++ b/core/modules/file/lib/Drupal/file/FileStorage.php
@@ -38,4 +38,60 @@ public function retrieveTemporaryFiles() {
       ':changed' => REQUEST_TIME - DRUPAL_MAXIMUM_TEMP_FILE_AGE
     ));
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildSchema() {
+    $schema = parent::buildSchema();
+
+    $schema['file_usage'] = array(
+      'description' => 'Track where a file is used.',
+      'fields' => array(
+        'fid' => array(
+          'description' => 'File ID.',
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+        ),
+        'module' => array(
+          'description' => 'The name of the module that is using the file.',
+          'type' => 'varchar',
+          'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
+          'not null' => TRUE,
+          'default' => '',
+        ),
+        'type' => array(
+          'description' => 'The name of the object type in which the file is used.',
+          'type' => 'varchar',
+          'length' => 64,
+          'not null' => TRUE,
+          'default' => '',
+        ),
+        'id' => array(
+          'description' => 'The primary key of the object using the file.',
+          'type' => 'varchar',
+          'length' => 64,
+          'not null' => TRUE,
+          'default' => 0,
+        ),
+        'count' => array(
+          'description' => 'The number of times this file is used by this object.',
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+        ),
+      ),
+      'primary key' => array('fid', 'type', 'id', 'module'),
+      'indexes' => array(
+        'type_id' => array('type', 'id'),
+        'fid_count' => array('fid', 'count'),
+        'fid_module' => array('fid', 'module'),
+      ),
+    );
+
+    return $schema;
+  }
+
 }
diff --git a/core/modules/file/lib/Drupal/file/Tests/FileItemTest.php b/core/modules/file/lib/Drupal/file/Tests/FileItemTest.php
index 0f40a14..af73f03 100644
--- a/core/modules/file/lib/Drupal/file/Tests/FileItemTest.php
+++ b/core/modules/file/lib/Drupal/file/Tests/FileItemTest.php
@@ -42,8 +42,7 @@ public static function getInfo() {
   public function setUp() {
     parent::setUp();
 
-    $this->installSchema('file', 'file_managed');
-    $this->installSchema('file', 'file_usage');
+    $this->installEntitySchema('file');
 
     entity_create('field_config', array(
       'name' => 'file_test',
diff --git a/core/modules/file/lib/Drupal/file/Tests/FileManagedTestBase.php b/core/modules/file/lib/Drupal/file/Tests/FileManagedTestBase.php
index c777691..91edf6b 100644
--- a/core/modules/file/lib/Drupal/file/Tests/FileManagedTestBase.php
+++ b/core/modules/file/lib/Drupal/file/Tests/FileManagedTestBase.php
@@ -150,22 +150,19 @@ function assertSameFile(FileInterface $file1, FileInterface $file2) {
    *   File entity.
    */
   function createFile($filepath = NULL, $contents = NULL, $scheme = NULL) {
-    $file = new \stdClass();
-    $file->uri = $this->createUri($filepath, $contents, $scheme);
-    $file->filename = drupal_basename($file->uri);
-    $file->filemime = 'text/plain';
-    $file->uid = 1;
-    $file->created = REQUEST_TIME;
-    $file->changed = REQUEST_TIME;
-    $file->filesize = filesize($file->uri);
-    $file->status = 0;
+    // Don't count hook invocations caused by creating the file.
+    \Drupal::state()->set('file_test.count_hook_invocations', FALSE);
+    $file = entity_create('file', array(
+      'uri' => $this->createUri($filepath, $contents, $scheme),
+      'uid' => 1,
+    ));
+    $file->save();
     // Write the record directly rather than using the API so we don't invoke
     // the hooks.
-    $file = (array) $file;
-    $file['fid'] = db_insert('file_managed')
-      ->fields($file)
-      ->execute();
-    return entity_create('file', $file);
+    $this->assertTrue($file->id() > 0, 'The file was added to the database.', 'Create test file');
+
+    \Drupal::state()->set('file_test.count_hook_invocations', TRUE);
+    return $file;
   }
 
   /**
diff --git a/core/modules/file/lib/Drupal/file/Tests/FileManagedUnitTestBase.php b/core/modules/file/lib/Drupal/file/Tests/FileManagedUnitTestBase.php
index c9ec86d..1930297 100644
--- a/core/modules/file/lib/Drupal/file/Tests/FileManagedUnitTestBase.php
+++ b/core/modules/file/lib/Drupal/file/Tests/FileManagedUnitTestBase.php
@@ -29,8 +29,8 @@ function setUp() {
     file_test_reset();
 
     $this->installConfig(array('system'));
-    $this->installSchema('file', array('file_managed', 'file_usage'));
-    $this->installSchema('user', array('users', 'users_roles'));
+    $this->installEntitySchema('file');
+    $this->installEntitySchema('user');
 
     // Make sure that a user with uid 1 exists, self::createFile() relies on
     // it.
@@ -161,20 +161,19 @@ function assertSameFile(FileInterface $file1, FileInterface $file2) {
    *   File entity.
    */
   function createFile($filepath = NULL, $contents = NULL, $scheme = NULL) {
-    $file = new \stdClass();
-    $file->uri = $this->createUri($filepath, $contents, $scheme);
-    $file->filename = drupal_basename($file->uri);
-    $file->filemime = 'text/plain';
-    $file->uid = 1;
-    $file->created = REQUEST_TIME;
-    $file->changed = REQUEST_TIME;
-    $file->filesize = filesize($file->uri);
-    $file->status = 0;
+    // Don't count hook invocations caused by creating the file.
+    \Drupal::state()->set('file_test.count_hook_invocations', FALSE);
+    $file = entity_create('file', array(
+      'uri' => $this->createUri($filepath, $contents, $scheme),
+      'uid' => 1,
+    ));
+    $file->save();
     // Write the record directly rather than using the API so we don't invoke
     // the hooks.
-    $this->assertNotIdentical(drupal_write_record('file_managed', $file), FALSE, 'The file was added to the database.', 'Create test file');
+    $this->assertTrue($file->id() > 0, 'The file was added to the database.', 'Create test file');
 
-    return entity_create('file', (array) $file);
+    \Drupal::state()->set('file_test.count_hook_invocations', TRUE);
+    return $file;
   }
 
   /**
diff --git a/core/modules/file/lib/Drupal/file/Tests/SpaceUsedTest.php b/core/modules/file/lib/Drupal/file/Tests/SpaceUsedTest.php
index a0c8e33..bfedef6 100644
--- a/core/modules/file/lib/Drupal/file/Tests/SpaceUsedTest.php
+++ b/core/modules/file/lib/Drupal/file/Tests/SpaceUsedTest.php
@@ -23,20 +23,40 @@ function setUp() {
     parent::setUp();
 
     // Create records for a couple of users with different sizes.
-    $file = array('uid' => 2, 'uri' => 'public://example1.txt', 'filesize' => 50, 'status' => FILE_STATUS_PERMANENT);
-    db_insert('file_managed')->fields($file)->execute();
-    $file = array('uid' => 2, 'uri' => 'public://example2.txt', 'filesize' => 20, 'status' => FILE_STATUS_PERMANENT);
-    db_insert('file_managed')->fields($file)->execute();
-    $file = array('uid' => 3, 'uri' => 'public://example3.txt', 'filesize' => 100, 'status' => FILE_STATUS_PERMANENT);
-    db_insert('file_managed')->fields($file)->execute();
-    $file = array('uid' => 3, 'uri' => 'public://example4.txt', 'filesize' => 200, 'status' => FILE_STATUS_PERMANENT);
-    db_insert('file_managed')->fields($file)->execute();
+    $this->createFileWithSize('public://example1.txt', 50, 2);
+    $this->createFileWithSize('public://example2.txt', 20, 2);
+    $this->createFileWithSize('public://example3.txt', 100, 3);
+    $this->createFileWithSize('public://example4.txt', 200, 3);
 
     // Now create some non-permanent files.
-    $file = array('uid' => 2, 'uri' => 'public://example5.txt', 'filesize' => 1, 'status' => 0);
-    db_insert('file_managed')->fields($file)->execute();
-    $file = array('uid' => 3, 'uri' => 'public://example6.txt', 'filesize' => 3, 'status' => 0);
-    db_insert('file_managed')->fields($file)->execute();
+    $this->createFileWithSize('public://example5.txt', 1, 2, 0);
+    $this->createFileWithSize('public://example6.txt', 3, 3, 0);
+  }
+
+  /**
+   * Creates a file with a given size.
+   *
+   * @param string $uri
+   *   URI of the file to create.
+   * @param int $size
+   *   Size of the file.
+   * @param int $uid
+   *   File owner ID.
+   * @param int $status
+   *   Whether the file should be permanent or temporary.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   The file entity.
+   */
+  protected function createFileWithSize($uri, $size, $uid, $status = FILE_STATUS_PERMANENT) {
+    file_put_contents($uri, $this->randomName($size));
+    $file = entity_create('file', array(
+      'uri' => $uri,
+      'uid' => $uid,
+      'status' => $status,
+    ));
+    $file->save();
+    return $file;
   }
 
   /**
diff --git a/core/modules/file/tests/file_test/file_test.module b/core/modules/file/tests/file_test/file_test.module
index e5a10a0..5fcd15f 100644
--- a/core/modules/file/tests/file_test/file_test.module
+++ b/core/modules/file/tests/file_test/file_test.module
@@ -108,9 +108,11 @@ function file_test_get_all_calls() {
  * @see file_test_reset()
  */
 function _file_test_log_call($op, $args) {
-  $results = \Drupal::state()->get('file_test.results') ?: array();
-  $results[$op][] = $args;
-  \Drupal::state()->set('file_test.results', $results);
+  if (\Drupal::state()->get('file_test.count_hook_invocations', TRUE)) {
+    $results = \Drupal::state()->get('file_test.results') ?: array();
+    $results[$op][] = $args;
+    \Drupal::state()->set('file_test.results', $results);
+  }
 }
 
 /**
diff --git a/core/modules/filter/lib/Drupal/filter/Tests/FilterDefaultConfigTest.php b/core/modules/filter/lib/Drupal/filter/Tests/FilterDefaultConfigTest.php
index 9d8f3ca..7423b70 100644
--- a/core/modules/filter/lib/Drupal/filter/Tests/FilterDefaultConfigTest.php
+++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterDefaultConfigTest.php
@@ -30,7 +30,7 @@ function setUp() {
     // filter_permission() calls into url() to output a link in the description.
     $this->installSchema('system', 'url_alias');
 
-    $this->installSchema('user', array('users_roles'));
+    $this->installEntitySchema('user');
 
     // Install filter_test module, which ships with custom default format.
     $this->installConfig(array('user', 'filter_test'));
diff --git a/core/modules/forum/forum.install b/core/modules/forum/forum.install
index 7f975e9..70b53b0 100644
--- a/core/modules/forum/forum.install
+++ b/core/modules/forum/forum.install
@@ -35,16 +35,6 @@ function forum_install() {
         ),
       ))->save();
 
-      // Create a default forum so forum posts can be created.
-      $term = entity_create('taxonomy_term', array(
-        'name' => t('General discussion'),
-        'description' => '',
-        'parent' => array(0),
-        'vid' => 'forums',
-        'forum_container' => 0,
-      ));
-      $term->save();
-
       // Create the instance on the bundle.
       entity_create('field_instance_config', array(
         'field_name' => 'taxonomy_forums',
@@ -68,7 +58,6 @@ function forum_install() {
           'weight' => 10,
         ))
         ->save();
-
       entity_get_display('node', 'forum', 'teaser')
         ->setComponent('taxonomy_forums', array(
           'type' => 'taxonomy_term_reference_link',
@@ -89,6 +78,22 @@ function forum_install() {
 }
 
 /**
+ * Implements hook_entity_schema_installed().
+ */
+function forum_entity_schema_installed(array $storages) {
+  if (isset($storages['taxonomy_term'])) {
+    // Create a default forum so forum posts can be created.
+    $storages['taxonomy_term']->create(array(
+      'name' => t('General discussion'),
+      'description' => '',
+      'parent' => array(0),
+      'vid' => 'forums',
+      'forum_container' => 0,
+    ))->save();
+  }
+}
+
+/**
  * Implements hook_uninstall().
  */
 function forum_uninstall() {
diff --git a/core/modules/hal/lib/Drupal/hal/Tests/EntityTest.php b/core/modules/hal/lib/Drupal/hal/Tests/EntityTest.php
index 42bfc4f..ce8ec46 100644
--- a/core/modules/hal/lib/Drupal/hal/Tests/EntityTest.php
+++ b/core/modules/hal/lib/Drupal/hal/Tests/EntityTest.php
@@ -38,9 +38,8 @@ function setUp() {
 
     \Drupal::service('router.builder')->rebuild();
     $this->installSchema('system', array('sequences'));
-    $this->installSchema('node', array('node', 'node_field_data', 'node_revision', 'node_field_revision'));
-    $this->installSchema('user', array('users_roles'));
-    $this->installSchema('taxonomy', array('taxonomy_term_data', 'taxonomy_term_hierarchy'));
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('taxonomy_term');
   }
 
   /**
@@ -70,15 +69,20 @@ public function testNode() {
     $original_values = $node->toArray();
     unset($original_values['nid']);
     unset($original_values['vid']);
+    unset($original_values['uid']);
 
     $normalized = $this->serializer->normalize($node, $this->format);
 
+    /** @var \Drupal\node\NodeInterface $denormalized_node */
     $denormalized_node = $this->serializer->denormalize($normalized, 'Drupal\node\Entity\Node', $this->format);
 
     // Verify that the ID and revision ID were skipped by the normalizer.
     $this->assertEqual(NULL, $denormalized_node->id());
     $this->assertEqual(NULL, $denormalized_node->getRevisionId());
 
+    // Verify that the owner ID was normalized and denormalized properly.
+    $this->assertEqual($user->id(), $denormalized_node->getOwnerId());
+
     // Loop over the remaining fields and verify that they are identical.
     foreach ($original_values as $field_name => $field_values) {
       $this->assertEqual($field_values, $denormalized_node->get($field_name)->getValue());
diff --git a/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php b/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php
index 98d24ce..0dc3ec0 100644
--- a/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php
+++ b/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php
@@ -62,8 +62,8 @@
   function setUp() {
     parent::setUp();
     $this->installSchema('system', array('url_alias', 'router'));
-    $this->installSchema('user', array('users'));
-    $this->installSchema('entity_test', array('entity_test'));
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('entity_test');
     $this->installConfig(array('field', 'language'));
 
     // Add English as a language.
diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageItemTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageItemTest.php
index 7803a11..7f16d09 100644
--- a/core/modules/image/lib/Drupal/image/Tests/ImageItemTest.php
+++ b/core/modules/image/lib/Drupal/image/Tests/ImageItemTest.php
@@ -47,7 +47,7 @@ public static function getInfo() {
   public function setUp() {
     parent::setUp();
 
-    $this->installSchema('file', array('file_managed', 'file_usage'));
+    $this->installEntitySchema('file');
 
     entity_create('field_config', array(
       'name' => 'image_test',
diff --git a/core/modules/node/lib/Drupal/node/Entity/Node.php b/core/modules/node/lib/Drupal/node/Entity/Node.php
index d5aea78..1faf801 100644
--- a/core/modules/node/lib/Drupal/node/Entity/Node.php
+++ b/core/modules/node/lib/Drupal/node/Entity/Node.php
@@ -24,6 +24,7 @@
  *   label = @Translation("Content"),
  *   bundle_label = @Translation("Content type"),
  *   controllers = {
+ *     "storage" = "Drupal\node\NodeStorage",
  *     "view_builder" = "Drupal\node\NodeViewBuilder",
  *     "access" = "Drupal\node\NodeAccessController",
  *     "form" = {
@@ -387,7 +388,6 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
       ->setRevisionable(TRUE)
       ->setSettings(array(
         'target_type' => 'user',
-        'default_value' => 0,
       ))
       ->setTranslatable(TRUE);
 
@@ -424,6 +424,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
     $fields['revision_timestamp'] = FieldDefinition::create('timestamp')
       ->setLabel(t('Revision timestamp'))
       ->setDescription(t('The time that the current revision was created.'))
+      ->setSetting('default_value', 0)
       ->setQueryable(FALSE)
       ->setRevisionable(TRUE);
 
@@ -448,11 +449,32 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    */
   public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
     $node_type = node_type_load($bundle);
+
     $fields = array();
+
+    // When deleting a node type the corresponding node displays are deleted as
+    // well. In order to be deleted, they need to be loaded first. Entity
+    // displays, however, fetch the field definitions of the respective entity
+    // type to fill in their defaults. Therefore this function ends up being
+    // called with a non-existing bundle.
+    // @todo Fix this!
+    if (!$node_type) {
+      return $fields;
+    }
+
     if (isset($node_type->title_label)) {
       $fields['title'] = clone $base_field_definitions['title'];
       $fields['title']->setLabel($node_type->title_label);
     }
+
+    $options = $node_type->getModuleSettings('node')['options'];
+    $fields['status'] = clone $base_field_definitions['status'];
+    $fields['status']->setSetting('default_value', !empty($options['status']) ? NODE_PUBLISHED : NODE_NOT_PUBLISHED);
+    $fields['promote'] = clone $base_field_definitions['promote'];
+    $fields['promote']->setSetting('default_value', !empty($options['promote']) ? NODE_PROMOTED : NODE_NOT_PROMOTED);
+    $fields['sticky'] = clone $base_field_definitions['sticky'];
+    $fields['sticky']->setSetting('default_value', !empty($options['sticky']) ? NODE_STICKY : NODE_NOT_STICKY);
+
     return $fields;
   }
 
diff --git a/core/modules/node/lib/Drupal/node/NodeFormController.php b/core/modules/node/lib/Drupal/node/NodeFormController.php
index da55f5f..c1e927e 100644
--- a/core/modules/node/lib/Drupal/node/NodeFormController.php
+++ b/core/modules/node/lib/Drupal/node/NodeFormController.php
@@ -32,20 +32,7 @@ class NodeFormController extends ContentEntityFormController {
   protected function prepareEntity() {
     /** @var \Drupal\node\NodeInterface $node */
     $node = $this->entity;
-    // Set up default values, if required.
-    $type = entity_load('node_type', $node->bundle());
-    $this->settings = $type->getModuleSettings('node');
-
-    // If this is a new node, fill in the default values.
-    if ($node->isNew()) {
-      foreach (array('status', 'promote', 'sticky') as $key) {
-        // Multistep node forms might have filled in something already.
-        if ($node->$key->isEmpty()) {
-          $node->$key = (int) !empty($this->settings['options'][$key]);
-        }
-      }
-    }
-    else {
+    if (!$node->isNew()) {
       $node->date = format_date($node->getCreatedTime(), 'custom', 'Y-m-d H:i:s O');
       // Remove the log message from the original node entity.
       $node->log = NULL;
diff --git a/core/modules/node/lib/Drupal/node/NodeStorage.php b/core/modules/node/lib/Drupal/node/NodeStorage.php
new file mode 100644
index 0000000..7f52a36
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/NodeStorage.php
@@ -0,0 +1,100 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\node\NodeStorage.
+ */
+
+namespace Drupal\node;
+
+use Drupal\Core\Entity\ContentEntityDatabaseStorage;
+
+/**
+ * Provides storage for the 'node' entity type.
+ */
+class NodeStorage extends ContentEntityDatabaseStorage {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSchema() {
+    $schema = parent::getSchema();
+
+    // @todo Theoretically this table belongs to NodeGrantDatabaseStorage
+    //   instead of NodeStorage. Add a schema creation facility to the former
+    //   similar to this one.
+    $schema['node_access'] = array(
+      'description' => 'Identifies which realm/grant pairs a user must possess in order to view, update, or delete specific nodes.',
+      'fields' => array(
+        'nid' => array(
+          'description' => 'The {node}.nid this record affects.',
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+        ),
+        'langcode' => array(
+          'description' => 'The {language}.langcode of this node.',
+          'type' => 'varchar',
+          'length' => 12,
+          'not null' => TRUE,
+          'default' => '',
+        ),
+        'fallback' => array(
+          'description' => 'Boolean indicating whether this record should be used as a fallback if a language condition is not provided.',
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 1,
+        ),
+        'gid' => array(
+          'description' => "The grant ID a user must possess in the specified realm to gain this row's privileges on the node.",
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+        ),
+        'realm' => array(
+          'description' => 'The realm in which the user must possess the grant ID. Each node access node can define one or more realms.',
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+          'default' => '',
+        ),
+        'grant_view' => array(
+          'description' => 'Boolean indicating whether a user with the realm/grant pair can view this node.',
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+          'size' => 'tiny',
+        ),
+        'grant_update' => array(
+          'description' => 'Boolean indicating whether a user with the realm/grant pair can edit this node.',
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+          'size' => 'tiny',
+        ),
+        'grant_delete' => array(
+          'description' => 'Boolean indicating whether a user with the realm/grant pair can delete this node.',
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+          'size' => 'tiny',
+        ),
+      ),
+      'primary key' => array('nid', 'gid', 'realm', 'langcode'),
+      'foreign keys' => array(
+        'affected_node' => array(
+          'table' => 'node',
+          'columns' => array('nid' => 'nid'),
+        ),
+      ),
+    );
+
+    return $schema;
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Tests/Condition/NodeConditionTest.php b/core/modules/node/lib/Drupal/node/Tests/Condition/NodeConditionTest.php
index 065cdca..a52b9fa 100644
--- a/core/modules/node/lib/Drupal/node/Tests/Condition/NodeConditionTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/Condition/NodeConditionTest.php
@@ -26,7 +26,7 @@ public static function getInfo() {
 
   public function setUp() {
     parent::setUp();
-    $this->installSchema('node', array('node', 'node_field_data', 'node_field_revision', 'node_revision'));
+    $this->installEntitySchema('node');
 
     // Create the node bundles required for testing.
     $type = entity_create('node_type', array('type' => 'page', 'name' => 'page'));
diff --git a/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php b/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php
index 1d59ca1..dcf4e7e 100644
--- a/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php
@@ -28,7 +28,7 @@ public function setUp() {
     parent::setUp();
 
     $this->installSchema('system', array('config_snapshot'));
-    $this->installSchema('user', array('users'));
+    $this->installEntitySchema('user');
 
     // Set default storage backend.
     $this->installConfig(array('field'));
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeFormButtonsTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeFormButtonsTest.php
index 1aa7dcd..9a6831d 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeFormButtonsTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeFormButtonsTest.php
@@ -108,7 +108,10 @@ function testNodeFormButtons() {
     // Set article content type default to unpublished. This will change the
     // the initial order of buttons and/or status of the node when creating
     // a node.
-    \Drupal::config('node.type.article')->set('settings.node.options.status', FALSE)->save();
+    /** @var \Drupal\node\NodeTypeInterface $node_type */
+    $node_type = $this->container->get('entity.manager')->getStorage('node_type')->load('article');
+    $node_type->settings['node']['options']['status'] = FALSE;
+    $node_type->save();
 
     // Verify the buttons on a node add form for an administrator.
     $this->drupalLogin($this->admin_user);
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeLastChangedTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeLastChangedTest.php
index bf23498..aa1dbfa 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeLastChangedTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeLastChangedTest.php
@@ -31,11 +31,8 @@ public static function getInfo() {
 
   public function setUp() {
     parent::setUp();
-    $this->installSchema('node', 'node');
-    $this->installSchema('node', 'node_revision');
-    $this->installSchema('node', 'node_field_data');
-    $this->installSchema('node', 'node_field_revision');
-    $this->installSchema('user', array('users'));
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
   }
 
   /**
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeTokenReplaceTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeTokenReplaceTest.php
index 717abcc..2b1e07f 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeTokenReplaceTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeTokenReplaceTest.php
@@ -38,7 +38,7 @@ public static function getInfo() {
    */
   public function setUp() {
     parent::setUp();
-    $this->installSchema('node', array('node', 'node_field_revision', 'node_field_data', 'node_revision'));
+    $this->installEntitySchema('node');
     $this->installConfig(array('filter'));
 
     $node_type = entity_create('node_type', array('type' => 'article', 'name' => 'Article'));
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeValidationTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeValidationTest.php
index e71d75b..94c7ca3 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeValidationTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeValidationTest.php
@@ -34,7 +34,7 @@ public static function getInfo() {
    */
   public function setUp() {
     parent::setUp();
-    $this->installSchema('node', array('node', 'node_field_data', 'node_field_revision', 'node_revision'));
+    $this->installEntitySchema('node');
 
     // Create a node type for testing.
     $type = entity_create('node_type', array('type' => 'page', 'name' => 'page'));
diff --git a/core/modules/node/node.install b/core/modules/node/node.install
index 36f8345..86546c3 100644
--- a/core/modules/node/node.install
+++ b/core/modules/node/node.install
@@ -36,390 +36,6 @@ function node_requirements($phase) {
 }
 
 /**
- * Implements hook_schema().
- */
-function node_schema() {
-  $schema['node'] = array(
-    'description' => 'The base table for nodes.',
-    'fields' => array(
-      'nid' => array(
-        'description' => 'The primary identifier for a node.',
-        'type' => 'serial',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-      ),
-      'uuid' => array(
-        'description' => 'Unique Key: Universally unique identifier for this entity.',
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => FALSE,
-      ),
-      // Defaults to NULL in order to avoid a brief period of potential
-      // deadlocks on the index.
-      'vid' => array(
-        'description' => 'The current {node_field_revision}.vid version identifier.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => FALSE,
-        'default' => NULL,
-      ),
-      'type' => array(
-        'description' => 'The type of this node.',
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-    ),
-    'indexes' => array(
-      'node_type' => array(array('type', 4)),
-    ),
-    'unique keys' => array(
-      'vid' => array('vid'),
-      'uuid' => array('uuid'),
-    ),
-    'foreign keys' => array(
-      'node_revision' => array(
-        'table' => 'node_revision',
-        'columns' => array('vid' => 'vid'),
-      ),
-    ),
-    'primary key' => array('nid'),
-  );
-
-  $schema['node_revision'] = array(
-    'description' => 'Stores information about each saved version of a {node}.',
-    'fields' => array(
-      'nid' => array(
-        'description' => 'The {node} this version belongs to.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-      ),
-      'vid' => array(
-        'description' => 'The primary identifier for this version.',
-        'type' => 'serial',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-      ),
-      'revision_uid' => array(
-        'description' => 'The {users}.uid that created this version.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'log' => array(
-        'description' => 'The log entry explaining the changes in this version.',
-        'type' => 'text',
-        'not null' => FALSE,
-        'size' => 'big',
-      ),
-      'revision_timestamp' => array(
-        'description' => 'The Unix timestamp when the version was created.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of this version.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-    ),
-    'indexes' => array(
-      'nid' => array('nid'),
-      'revision_uid' => array('revision_uid'),
-      'node_langcode' => array('langcode'),
-    ),
-    'foreign keys' => array(
-      'versioned_node' => array(
-        'table' => 'node',
-        'columns' => array('nid' => 'nid'),
-      ),
-      'version_author' => array(
-        'table' => 'users',
-        'columns' => array('revision_uid' => 'uid'),
-      ),
-    ),
-    'primary key' => array('vid'),
-  );
-
-  // Node field storage.
-  $schema['node_field_data'] = array(
-    'description' => 'Data table for node base fields.',
-    'fields' => array(
-      'nid' => array(
-        'description' => 'The primary identifier for a node.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-      ),
-      'vid' => array(
-        'description' => 'The current {node_field_revision}.vid version identifier.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-      ),
-      'type' => array(
-        'description' => 'The {node_type}.type of this node.',
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of these node property values.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'default_langcode' => array(
-        'description' => 'Boolean indicating whether the property values are in the {language}.langcode of this node.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 1,
-      ),
-      'title' => array(
-        'description' => 'The title of this node, always treated as non-markup plain text.',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'uid' => array(
-        'description' => 'The {users}.uid that owns this node; initially, this is the user that created it.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'status' => array(
-        'description' => 'Boolean indicating whether the node translation is published (visible to non-administrators).',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 1,
-      ),
-      'created' => array(
-        'description' => 'The Unix timestamp when the node translation was created.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'changed' => array(
-        'description' => 'The Unix timestamp when the node translation was most recently saved.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'promote' => array(
-        'description' => 'Boolean indicating whether the node translation should be displayed on the front page.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'sticky' => array(
-        'description' => 'Boolean indicating whether the node translation should be displayed at the top of lists in which it appears.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-    ),
-    '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)),
-      'node_type' => array(array('type', 4)),
-      'vid' => array('vid'),
-      'uid' => array('uid'),
-    ),
-    'foreign keys' => array(
-      'node_base' => array(
-        'table' => 'node',
-        'columns' => array('nid' => 'nid'),
-      ),
-      'node_author' => array(
-        'table' => 'users',
-        'columns' => array('uid' => 'uid'),
-      ),
-    ),
-    'primary key' => array('nid', 'langcode'),
-  );
-
-  $schema['node_field_revision'] = array(
-    'description' => 'Revision table for node base fields.',
-    'fields' => array(
-      'nid' => array(
-        'description' => 'The {node} this version belongs to.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-      ),
-      'vid' => array(
-        'description' => 'The primary identifier for this version.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of this version.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'default_langcode' => array(
-        'description' => 'Boolean indicating whether the property values of this version are in the {language}.langcode of this node.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 1,
-      ),
-      'title' => array(
-        'description' => 'The title of this version, always treated as non-markup plain text.',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'uid' => array(
-        'description' => 'The {users}.uid that created this node.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'status' => array(
-        'description' => 'Boolean indicating whether the node (at the time of this revision) is published (visible to non-administrators).',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 1,
-      ),
-      'created' => array(
-        'description' => 'The Unix timestamp when the node was created.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'changed' => array(
-        'description' => 'The Unix timestamp when the version was most recently saved.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'promote' => array(
-        'description' => 'Boolean indicating whether the node (at the time of this revision) should be displayed on the front page.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'sticky' => array(
-        'description' => 'Boolean indicating whether the node (at the time of this revision) should be displayed at the top of lists in which it appears.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-    ),
-    'indexes' => array(
-      'uid' => array('uid'),
-      'node_default_langcode' => array('default_langcode'),
-      'node_langcode' => array('langcode'),
-    ),
-    'foreign keys' => array(
-      'versioned_node' => array(
-        'table' => 'node',
-        'columns' => array('nid' => 'nid'),
-      ),
-      'node_author' => array(
-        'table' => 'users',
-        'columns' => array('uid' => 'uid'),
-      ),
-    ),
-    'primary key' => array('vid', 'langcode'),
-  );
-
-  $schema['node_access'] = array(
-    'description' => 'Identifies which realm/grant pairs a user must possess in order to view, update, or delete specific nodes.',
-    'fields' => array(
-      'nid' => array(
-        'description' => 'The {node}.nid this record affects.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of this node.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'fallback' => array(
-        'description' => 'Boolean indicating whether this record should be used as a fallback if a language condition is not provided.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 1,
-      ),
-      'gid' => array(
-        'description' => "The grant ID a user must possess in the specified realm to gain this row's privileges on the node.",
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'realm' => array(
-        'description' => 'The realm in which the user must possess the grant ID. Each node access node can define one or more realms.',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'grant_view' => array(
-        'description' => 'Boolean indicating whether a user with the realm/grant pair can view this node.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-        'size' => 'tiny',
-      ),
-      'grant_update' => array(
-        'description' => 'Boolean indicating whether a user with the realm/grant pair can edit this node.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-        'size' => 'tiny',
-      ),
-      'grant_delete' => array(
-        'description' => 'Boolean indicating whether a user with the realm/grant pair can delete this node.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-        'size' => 'tiny',
-      ),
-    ),
-    'primary key' => array('nid', 'gid', 'realm', 'langcode'),
-    'foreign keys' => array(
-      'affected_node' => array(
-        'table' => 'node',
-        'columns' => array('nid' => 'nid'),
-      ),
-    ),
-  );
-
-  return $schema;
-}
-
-/**
  * Implements hook_install().
  */
 function node_install() {
@@ -434,18 +50,25 @@ function node_install() {
     user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access content'));
     user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, array('access content'));
   }
+}
 
-  // Populate the node access table.
-  db_insert('node_access')
-    ->fields(array(
-      'nid' => 0,
-      'gid' => 0,
-      'realm' => 'all',
-      'grant_view' => 1,
-      'grant_update' => 0,
-      'grant_delete' => 0,
-    ))
-    ->execute();
+/**
+ * Implements hook_entity_schema_installed().
+ */
+function node_entity_schema_installed(array $storages) {
+  if (isset($storages['node'])) {
+    // Populate the node access table.
+    db_insert('node_access')
+      ->fields(array(
+        'nid' => 0,
+        'gid' => 0,
+        'realm' => 'all',
+        'grant_view' => 1,
+        'grant_update' => 0,
+        'grant_delete' => 0,
+      ))
+      ->execute();
+  }
 }
 
 /**
diff --git a/core/modules/rdf/lib/Drupal/rdf/Tests/CommentAttributesTest.php b/core/modules/rdf/lib/Drupal/rdf/Tests/CommentAttributesTest.php
index f152e0b..d3cacfd 100644
--- a/core/modules/rdf/lib/Drupal/rdf/Tests/CommentAttributesTest.php
+++ b/core/modules/rdf/lib/Drupal/rdf/Tests/CommentAttributesTest.php
@@ -163,7 +163,7 @@ public function testCommentRdfaMarkup() {
     $anonymous_user['name'] = $this->randomName();
     $anonymous_user['mail'] = 'tester@simpletest.org';
     $anonymous_user['homepage'] = 'http://example.org/';
-    $comment2 = $this->saveComment($this->node->id(), NULL, $anonymous_user);
+    $comment2 = $this->saveComment($this->node->id(), 0, $anonymous_user);
 
     // Tests comment #2 as anonymous user.
     $parser = new \EasyRdf_Parser_Rdfa();
diff --git a/core/modules/rdf/lib/Drupal/rdf/Tests/Field/TaxonomyTermReferenceRdfaTest.php b/core/modules/rdf/lib/Drupal/rdf/Tests/Field/TaxonomyTermReferenceRdfaTest.php
index 00ac9d7..d8bfbed 100644
--- a/core/modules/rdf/lib/Drupal/rdf/Tests/Field/TaxonomyTermReferenceRdfaTest.php
+++ b/core/modules/rdf/lib/Drupal/rdf/Tests/Field/TaxonomyTermReferenceRdfaTest.php
@@ -50,7 +50,7 @@ public static function getInfo() {
   public function setUp() {
     parent::setUp();
 
-    $this->installSchema('taxonomy', array('taxonomy_term_data', 'taxonomy_term_hierarchy'));
+    $this->installEntitySchema('taxonomy_term');
 
     $vocabulary = entity_create('taxonomy_vocabulary', array(
       'name' => $this->randomName(),
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
index adff21d..78a8629 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
@@ -58,7 +58,6 @@ public function testPatchUpdate() {
     $patch_entity = entity_create($entity_type, $patch_values);
     // We don't want to overwrite the UUID.
     unset($patch_entity->uuid);
-    $patch_entity->save();
     $serialized = $serializer->serialize($patch_entity, $this->defaultFormat);
 
     // Update the entity over the REST API.
diff --git a/core/modules/serialization/lib/Drupal/serialization/Tests/EntitySerializationTest.php b/core/modules/serialization/lib/Drupal/serialization/Tests/EntitySerializationTest.php
index 865704d..1ad843a 100644
--- a/core/modules/serialization/lib/Drupal/serialization/Tests/EntitySerializationTest.php
+++ b/core/modules/serialization/lib/Drupal/serialization/Tests/EntitySerializationTest.php
@@ -80,18 +80,12 @@ public function testNormalize() {
       'id' => array(
         array('value' => 1),
       ),
-      'revision_id' => array(
-        array('value' => 1),
-      ),
       'uuid' => array(
         array('value' => $this->entity->uuid()),
       ),
       'langcode' => array(
         array('value' => Language::LANGCODE_NOT_SPECIFIED),
       ),
-      'default_langcode' => array(
-        array('value' => NULL),
-      ),
       'name' => array(
         array('value' => $this->values['name']),
       ),
@@ -101,6 +95,9 @@ public function testNormalize() {
       'user_id' => array(
         array('target_id' => $this->values['user_id']),
       ),
+      'revision_id' => array(
+        array('value' => 1),
+      ),
       'field_test_text' => array(
         array(
           'value' => $this->values['field_test_text']['value'],
@@ -141,13 +138,12 @@ public function testSerialize() {
     // order.
     $expected = array(
       'id' => '<id><value>' . $this->entity->id() . '</value></id>',
-      'revision_id' => '<revision_id><value>' . $this->entity->getRevisionId() . '</value></revision_id>',
       'uuid' => '<uuid><value>' . $this->entity->uuid() . '</value></uuid>',
       'langcode' => '<langcode><value>' . Language::LANGCODE_NOT_SPECIFIED . '</value></langcode>',
-      'default_langcode' => '<default_langcode><value/></default_langcode>',
       'name' => '<name><value>' . $this->values['name'] . '</value></name>',
       'type' => '<type><value>entity_test_mulrev</value></type>',
       'user_id' => '<user_id><target_id>' . $this->values['user_id'] . '</target_id></user_id>',
+      'revision_id' => '<revision_id><value>' . $this->entity->getRevisionId() . '</value></revision_id>',
       'field_test_text' => '<field_test_text><value>' . $this->values['field_test_text']['value'] . '</value><format>' . $this->values['field_test_text']['format'] . '</format></field_test_text>',
     );
     // Sort it in the same order as normalised.
diff --git a/core/modules/serialization/lib/Drupal/serialization/Tests/NormalizerTestBase.php b/core/modules/serialization/lib/Drupal/serialization/Tests/NormalizerTestBase.php
index 3b0cfd6..0c1dc03 100644
--- a/core/modules/serialization/lib/Drupal/serialization/Tests/NormalizerTestBase.php
+++ b/core/modules/serialization/lib/Drupal/serialization/Tests/NormalizerTestBase.php
@@ -21,8 +21,8 @@
   protected function setUp() {
     parent::setUp();
 
-    $this->installSchema('entity_test', array('entity_test_mulrev', 'entity_test_mulrev_revision', 'entity_test_mulrev_property_revision', 'entity_test_mulrev_property_data'));
-    $this->installSchema('user', array('users', 'users_roles'));
+    $this->installEntitySchema('entity_test_mulrev');
+    $this->installEntitySchema('user');
     $this->installSchema('system', array('url_alias'));
     $this->installConfig(array('field'));
 
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Entity/Shortcut.php b/core/modules/shortcut/lib/Drupal/shortcut/Entity/Shortcut.php
index 00d0dd3..f361bcd 100644
--- a/core/modules/shortcut/lib/Drupal/shortcut/Entity/Shortcut.php
+++ b/core/modules/shortcut/lib/Drupal/shortcut/Entity/Shortcut.php
@@ -183,10 +183,6 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
       ->setLabel(t('Language code'))
       ->setDescription(t('The language code of the shortcut.'));
 
-    $fields['default_langcode'] = FieldDefinition::create('boolean')
-      ->setLabel(t('Default language'))
-      ->setDescription(t('Flag to indicate whether this is the default language.'));
-
     $fields['path'] = FieldDefinition::create('string')
       ->setLabel(t('Path'))
       ->setDescription(t('The computed shortcut path.'))
diff --git a/core/modules/shortcut/shortcut.install b/core/modules/shortcut/shortcut.install
index b7ed49c..be6f8b2 100644
--- a/core/modules/shortcut/shortcut.install
+++ b/core/modules/shortcut/shortcut.install
@@ -12,96 +12,6 @@
  * Implements hook_schema().
  */
 function shortcut_schema() {
-  $schema['shortcut'] = array(
-    'description' => 'Stores shortcut items.',
-    'fields' => array(
-      'id' => array(
-        'type' => 'serial',
-        'not null' => TRUE,
-        'description' => 'Primary Key: Unique shortcut ID.',
-      ),
-      'uuid' => array(
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => FALSE,
-        'description' => 'Unique Key: Universally unique identifier for this shortcut.',
-      ),
-      'shortcut_set' => array(
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'The bundle of the shortcut.',
-      ),
-      'langcode' => array(
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'The {language}.langcode of the original variant of this shortcut.',
-      ),
-      'weight' => array(
-        'description' => 'Weight among shortcuts in the same shortcut set.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'route_name' => array(
-        'description' => 'The machine name of a defined Symfony Route this menu item represents.',
-        'type' => 'varchar',
-        'length' => 255,
-      ),
-      'route_parameters' => array(
-        'description' => 'Serialized array of route parameters of this shortcut.',
-        'type' => 'blob',
-        'size' => 'big',
-        'not null' => FALSE,
-        'serialize' => TRUE,
-      ),
-    ),
-    'primary key' => array('id'),
-    'unique keys' => array(
-      'uuid' => array('uuid'),
-    ),
-  );
-
-  $schema['shortcut_field_data'] = array(
-    'description' => 'Stores shortcut properties.',
-    'fields' => array(
-      'id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => 'The {shortcut}.id of the shortcut.',
-      ),
-      'langcode' => array(
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'The {language}.langcode of this variant of this shortcut.',
-      ),
-      'default_langcode' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 1,
-        'description' => 'Boolean indicating whether the current variant is in the original entity language.',
-      ),
-      'title' => array(
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => FALSE,
-        'description' => 'The title of the shortcut.',
-      ),
-    ),
-    'foreign keys' => array(
-      'shortcut' => array(
-        'table' => 'shortcut',
-        'columns' => array('id' => 'id'),
-      ),
-    ),
-    'primary key' => array('id', 'langcode'),
-  );
 
   $schema['shortcut_set_users'] = array(
     'description' => 'Maps users to shortcut sets.',
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php
index 36adb8b..4b9d02e 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php
@@ -7,8 +7,10 @@
 
 namespace Drupal\simpletest;
 
+use Drupal\Component\Utility\String;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\DrupalKernel;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandlerInterface;
 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
 use Drupal\Core\Language\Language;
 use Symfony\Component\DependencyInjection\Reference;
@@ -332,6 +334,37 @@ protected function installSchema($module, $tables) {
   }
 
   /**
+   * Installs the tables for a specific entity type.
+   *
+   * @param string $entity_type_id
+   *   The ID of the entity type.
+   */
+  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 ContentEntitySchemaHandlerInterface) {
+      $schema = $storage->getSchema();
+      foreach ($schema as $table_name => $table_schema) {
+        $schema_handler->createTable($table_name, $table_schema);
+      }
+
+      $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,
+      )));
+    }
+  }
+
+  /**
    * Enables modules for this test.
    *
    * @param array $modules
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/Tests/DrupalUnitTestBaseTest.php b/core/modules/simpletest/lib/Drupal/simpletest/Tests/DrupalUnitTestBaseTest.php
index 2acf138..e968c2f 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/Tests/DrupalUnitTestBaseTest.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/Tests/DrupalUnitTestBaseTest.php
@@ -19,7 +19,7 @@ class DrupalUnitTestBaseTest extends DrupalUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('entity', 'entity_test');
+  public static $modules = array('entity', 'entity_test', 'field', 'simpletest_test');
 
   public static function getInfo() {
     return array(
@@ -33,17 +33,18 @@ public static function getInfo() {
    * Tests expected behavior of setUp().
    */
   function testSetUp() {
-    $modules = array('entity', 'entity_test');
-    $table = 'entity_test';
+    $tables = array('entity_test', 'simpletest_test');
 
     // Verify that specified $modules have been loaded.
     $this->assertTrue(function_exists('entity_test_permission'), 'entity_test.module was loaded.');
     // Verify that there is a fixed module list.
-    $this->assertIdentical(array_keys(\Drupal::moduleHandler()->getModuleList()), $modules);
-    $this->assertIdentical(\Drupal::moduleHandler()->getImplementations('permission'), $modules);
+    $this->assertIdentical(array_keys(\Drupal::moduleHandler()->getModuleList()), static::$modules);
+    $this->assertIdentical(\Drupal::moduleHandler()->getImplementations('permission'), array('entity', 'entity_test'));
 
     // Verify that no modules have been installed.
-    $this->assertFalse(db_table_exists($table), "'$table' database table not found.");
+    foreach($tables as $table){
+      $this->assertFalse(db_table_exists($table), "'$table' database table not found.");
+    }
   }
 
   /**
@@ -74,8 +75,8 @@ function testEnableModulesLoad() {
    * Tests expected installation behavior of enableModules().
    */
   function testEnableModulesInstall() {
-    $module = 'node';
-    $table = 'node';
+    $module = 'ban';
+    $table = 'ban_ip';
 
     // Verify that the module does not exist yet.
     $this->assertFalse(\Drupal::moduleHandler()->moduleExists($module), "$module module not found.");
@@ -108,9 +109,9 @@ function testEnableModulesInstall() {
    */
   function testEnableModulesInstallContainer() {
     // Install Node module.
-    $this->enableModules(array('field', 'node'));
+    $this->enableModules(array('field', 'node', 'user'));
 
-    $this->installSchema('node', array('node', 'node_field_data'));
+    $this->installEntitySchema('node');
     // Perform an entity query against node.
     $query = \Drupal::entityQuery('node');
     // Disable node access checks, since User module is not enabled.
@@ -124,8 +125,8 @@ function testEnableModulesInstallContainer() {
    * Tests expected behavior of installSchema().
    */
   function testInstallSchema() {
-    $module = 'entity_test';
-    $table = 'entity_test';
+    $module = 'simpletest_test';
+    $table = 'simpletest_test';
     // Verify that we can install a table from the module schema.
     $this->installSchema($module, $table);
     $this->assertTrue(db_table_exists($table), "'$table' database table found.");
diff --git a/core/modules/simpletest/tests/modules/simpletest_test/simpletest_test.info.yml b/core/modules/simpletest/tests/modules/simpletest_test/simpletest_test.info.yml
new file mode 100644
index 0000000..6c98069
--- /dev/null
+++ b/core/modules/simpletest/tests/modules/simpletest_test/simpletest_test.info.yml
@@ -0,0 +1,7 @@
+name: Simpletest Test
+type: module
+description: 'Provides dummy hook implementations for use by SimpleTest tests.'
+package: Testing
+version: VERSION
+core: 8.x
+hidden: TRUE
diff --git a/core/modules/simpletest/tests/modules/simpletest_test/simpletest_test.install b/core/modules/simpletest/tests/modules/simpletest_test/simpletest_test.install
new file mode 100644
index 0000000..f9b16a0
--- /dev/null
+++ b/core/modules/simpletest/tests/modules/simpletest_test/simpletest_test.install
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Schema Test module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function simpletest_test_schema(){
+  $schema['simpletest_test'] = array(
+    'description' => 'Stores simpltest_test data.',
+    'fields' => array(
+      'sid' => array(
+        'type' => 'serial',
+        'not null' => TRUE,
+        'description' => 'Primary Key: Unique ID.',
+      ),
+      'uuid' => array(
+        'description' => 'Unique Key: Universally unique identifier for this entity.',
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => FALSE,
+      ),
+      'dummy_textfield' => array(
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'A Dummy text field.',
+      ),
+      'dummy_integer' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'description' => 'A dummy integer field.',
+      ),
+    ),
+    'indexes' => array(
+      'simpltest_test_index' => array('sid', 'dummy_integer'),
+    ),
+    'primary key' => array('sid'),
+    'unique keys' => array(
+      'uuid' => array('uuid'),
+    ),
+  );
+
+  return $schema;
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Action/ActionUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Action/ActionUnitTest.php
index 6f97da1..55ae046 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Action/ActionUnitTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Action/ActionUnitTest.php
@@ -45,7 +45,7 @@ protected function setUp() {
     parent::setUp();
 
     $this->actionManager = $this->container->get('plugin.manager.action');
-    $this->installSchema('user', array('users', 'users_roles'));
+    $this->installEntitySchema('user');
     $this->installSchema('system', array('sequences'));
   }
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/WriteRecordTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/WriteRecordTest.php
index ffe5fdb..da88eda 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Common/WriteRecordTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/WriteRecordTest.php
@@ -7,19 +7,20 @@
 
 namespace Drupal\system\Tests\Common;
 
+use Drupal\simpletest\DrupalUnitTestBase;
 use Drupal\simpletest\WebTestBase;
 
 /**
  * Tests writing of data records with drupal_write_record().
  */
-class WriteRecordTest extends WebTestBase {
+class WriteRecordTest extends DrupalUnitTestBase {
 
   /**
    * Modules to enable.
    *
    * @var array
    */
-  public static $modules = array('database_test', 'node');
+  public static $modules = array('database_test');
 
   public static function getInfo() {
     return array(
@@ -33,6 +34,8 @@ public static function getInfo() {
    * Tests the drupal_write_record() API function.
    */
   function testDrupalWriteRecord() {
+    $this->installSchema('database_test', array('test', 'test_null', 'test_serialized', 'test_composite_primary'));
+
     // Insert a record with no columns populated.
     $record = array();
     $insert_result = drupal_write_record('test', $record);
@@ -135,15 +138,14 @@ function testDrupalWriteRecord() {
     $this->assertTrue($update_result == SAVED_UPDATED, 'Correct value returned when a valid update is run without changing any values.');
 
     // Insert an object record for a table with a multi-field primary key.
-    $node_access = new \stdClass();
-    $node_access->nid = mt_rand();
-    $node_access->gid = mt_rand();
-    $node_access->realm = $this->randomName();
-    $insert_result = drupal_write_record('node_access', $node_access);
+    $composite_primary = new \stdClass();
+    $composite_primary->name = $this->randomName();
+    $composite_primary->age = mt_rand();
+    $insert_result = drupal_write_record('test_composite_primary', $composite_primary);
     $this->assertTrue($insert_result == SAVED_NEW, 'Correct value returned when a record is inserted with drupal_write_record() for a table with a multi-field primary key.');
 
     // Update the record.
-    $update_result = drupal_write_record('node_access', $node_access, array('nid', 'gid', 'realm'));
+    $update_result = drupal_write_record('test_composite_primary', $composite_primary, array('name', 'job'));
     $this->assertTrue($update_result == SAVED_UPDATED, 'Correct value returned when a record is updated with drupal_write_record() for a table with a multi-field primary key.');
   }
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityApiTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityApiTest.php
index a9f0105..5d2364c 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityApiTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityApiTest.php
@@ -25,16 +25,10 @@ public static function getInfo() {
 
   public function setUp() {
     parent::setUp();
-    $this->installSchema('entity_test', array(
-      'entity_test_mul',
-      'entity_test_mul_property_data',
-      'entity_test_rev',
-      'entity_test_rev_revision',
-      'entity_test_mulrev',
-      'entity_test_mulrev_revision',
-      'entity_test_mulrev_property_data',
-      'entity_test_mulrev_property_revision'
-    ));
+
+    $this->installEntitySchema('entity_test_rev');
+    $this->installEntitySchema('entity_test_mul');
+    $this->installEntitySchema('entity_test_mulrev');
   }
 
   /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCrudHookTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCrudHookTest.php
index 1d49a02..6e5c980 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCrudHookTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCrudHookTest.php
@@ -44,9 +44,9 @@ public static function getInfo() {
 
   public function setUp() {
     parent::setUp();
-    $this->installSchema('user', array('users_data'));
-    $this->installSchema('node', array('node', 'node_revision', 'node_field_data', 'node_field_revision', 'node_access'));
-    $this->installSchema('comment', array('comment', 'comment_entity_statistics'));
+
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('comment');
   }
 
   /**
@@ -219,7 +219,8 @@ public function testCommentHooks() {
    * Tests hook invocations for CRUD operations on files.
    */
   public function testFileHooks() {
-    $this->installSchema('file', array('file_managed', 'file_usage'));
+    $this->installEntitySchema('file');
+
     $url = 'public://entity_crud_hook_test.file';
     file_put_contents($url, 'Test test test');
     $file = entity_create('file', array(
@@ -346,7 +347,7 @@ public function testNodeHooks() {
    * Tests hook invocations for CRUD operations on taxonomy terms.
    */
   public function testTaxonomyTermHooks() {
-    $this->installSchema('taxonomy', array('taxonomy_term_data', 'taxonomy_term_hierarchy'));
+    $this->installEntitySchema('taxonomy_term');
 
     $vocabulary = entity_create('taxonomy_vocabulary', array(
       'name' => 'Test vocabulary',
@@ -415,7 +416,7 @@ public function testTaxonomyTermHooks() {
    * Tests hook invocations for CRUD operations on taxonomy vocabularies.
    */
   public function testTaxonomyVocabularyHooks() {
-    $this->installSchema('taxonomy', array('taxonomy_term_data', 'taxonomy_term_hierarchy'));
+    $this->installEntitySchema('taxonomy_term');
 
     $vocabulary = entity_create('taxonomy_vocabulary', array(
       'name' => 'Test vocabulary',
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php
index 4a05431..36b3655 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php
@@ -40,18 +40,12 @@ public static function getInfo() {
 
   public function setUp() {
     parent::setUp();
-    $this->installSchema('user', array('users_data'));
-    $this->installSchema('node', array('node', 'node_revision', 'node_field_data', 'node_field_revision', 'node_access'));
-    $this->installSchema('entity_test', array(
-      'entity_test_mul',
-      'entity_test_mul_property_data',
-      'entity_test_rev',
-      'entity_test_rev_revision',
-      'entity_test_mulrev',
-      'entity_test_mulrev_revision',
-      'entity_test_mulrev_property_data',
-      'entity_test_mulrev_property_revision'
-    ));
+
+    $this->installEntitySchema('node');
+
+    $this->installEntitySchema('entity_test_rev');
+    $this->installEntitySchema('entity_test_mul');
+    $this->installEntitySchema('entity_test_mulrev');
 
     // Create the test field.
     entity_test_install();
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityLanguageTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityLanguageTestBase.php
index bd2ceb9..db45c92 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityLanguageTestBase.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityLanguageTestBase.php
@@ -50,16 +50,10 @@ function setUp() {
 
     $this->languageManager = $this->container->get('language_manager');
 
-    $this->installSchema('entity_test', array(
-      'entity_test_mul',
-      'entity_test_mul_property_data',
-      'entity_test_rev',
-      'entity_test_rev_revision',
-      'entity_test_mulrev',
-      'entity_test_mulrev_revision',
-      'entity_test_mulrev_property_data',
-      'entity_test_mulrev_property_revision',
-    ));
+    $this->installEntitySchema('entity_test_rev');
+    $this->installEntitySchema('entity_test_mul');
+    $this->installEntitySchema('entity_test_mulrev');
+
     $this->installConfig(array('language'));
 
     // Create the test field.
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryRelationshipTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryRelationshipTest.php
index e5d8cf4..8d67666 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryRelationshipTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryRelationshipTest.php
@@ -70,7 +70,7 @@ public static function getInfo() {
   public function setUp() {
     parent::setUp();
 
-    $this->installSchema('taxonomy', array('taxonomy_term_data', 'taxonomy_term_hierarchy'));
+    $this->installEntitySchema('taxonomy_term');
 
     // We want a taxonomy term reference field. It needs a vocabulary, terms,
     // a field and an instance. First, create the vocabulary.
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryTest.php
index c724da1..3123b63 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryTest.php
@@ -57,7 +57,9 @@ public static function getInfo() {
 
   function setUp() {
     parent::setUp();
-    $this->installSchema('entity_test', array('entity_test_mulrev', 'entity_test_mulrev_revision', 'entity_test_mulrev_property_data', 'entity_test_mulrev_property_revision'));
+
+    $this->installEntitySchema('entity_test_mulrev');
+
     $this->installConfig(array('language'));
 
     $figures = drupal_strtolower($this->randomName());
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUUIDTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUUIDTest.php
index ec7134c..a0ec726 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUUIDTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUUIDTest.php
@@ -23,16 +23,9 @@ public static function getInfo() {
   public function setUp() {
     parent::setUp();
 
-    $this->installSchema('entity_test', array(
-      'entity_test_mul',
-      'entity_test_mul_property_data',
-      'entity_test_rev',
-      'entity_test_rev_revision',
-      'entity_test_mulrev',
-      'entity_test_mulrev_revision',
-      'entity_test_mulrev_property_data',
-      'entity_test_mulrev_property_revision',
-    ));
+    $this->installEntitySchema('entity_test_rev');
+    $this->installEntitySchema('entity_test_mul');
+    $this->installEntitySchema('entity_test_mulrev');
   }
 
   /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUnitTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUnitTestBase.php
index 99fe76a..0c6aeab 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUnitTestBase.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUnitTestBase.php
@@ -42,9 +42,11 @@ public function setUp() {
     $this->entityManager = $this->container->get('entity.manager');
     $this->state = $this->container->get('state');
 
-    $this->installSchema('user', array('users', 'users_roles'));
     $this->installSchema('system', 'sequences');
-    $this->installSchema('entity_test', 'entity_test');
+
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('entity_test');
+
     $this->installConfig(array('field'));
   }
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityValidationTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityValidationTest.php
index 1346c7f..40d7708 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityValidationTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityValidationTest.php
@@ -37,17 +37,10 @@ public static function getInfo() {
    */
   public function setUp() {
     parent::setUp();
-    $this->installSchema('user', array('users_data'));
-    $this->installSchema('entity_test', array(
-      'entity_test_mul',
-      'entity_test_mul_property_data',
-      'entity_test_rev',
-      'entity_test_rev_revision',
-      'entity_test_mulrev',
-      'entity_test_mulrev_revision',
-      'entity_test_mulrev_property_data',
-      'entity_test_mulrev_property_revision'
-    ));
+
+    $this->installEntitySchema('entity_test_rev');
+    $this->installEntitySchema('entity_test_mul');
+    $this->installEntitySchema('entity_test_mulrev');
 
     // Create the test field.
     entity_test_install();
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/FieldAccessTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/FieldAccessTest.php
index 7395dfa..d72b297 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/FieldAccessTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/FieldAccessTest.php
@@ -44,8 +44,9 @@ protected function setUp() {
     // Install field configuration.
     $this->installConfig(array('field'));
     // The users table is needed for creating dummy user accounts.
-    $this->installSchema('user', array('users'));
+    $this->installEntitySchema('user');
     // Register entity_test text field.
+    module_load_install('entity_test');
     entity_test_install();
   }
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/FieldSqlStorageTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/FieldSqlStorageTest.php
index 82e63b2..de4aa63 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/FieldSqlStorageTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/FieldSqlStorageTest.php
@@ -66,7 +66,8 @@ public static function getInfo() {
 
   function setUp() {
     parent::setUp();
-    $this->installSchema('entity_test', array('entity_test_rev', 'entity_test_rev_revision'));
+
+    $this->installEntitySchema('entity_test_rev');
     $entity_type = 'entity_test_rev';
 
     $this->field_name = strtolower($this->randomName());
diff --git a/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php b/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php
index 5893f7b..37c4af9 100644
--- a/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php
@@ -44,7 +44,7 @@ public static function getInfo() {
   public function setUp() {
     parent::setup();
 
-    $this->installSchema('file', array('file_managed', "file_usage"));
+    $this->installEntitySchema('file');
     $this->typedDataManager = $this->container->get('typed_data_manager');
   }
 
diff --git a/core/modules/system/tests/modules/database_test/database_test.install b/core/modules/system/tests/modules/database_test/database_test.install
index 866d453..7c74c1c 100644
--- a/core/modules/system/tests/modules/database_test/database_test.install
+++ b/core/modules/system/tests/modules/database_test/database_test.install
@@ -245,5 +245,34 @@ function database_test_schema() {
     ),
   );
 
+  $schema['test_composite_primary'] = array(
+    'description' => 'Basic test table with a composite primary key',
+    'fields' => array(
+      'name' => array(
+        'description' => "A person's name",
+        'type' => 'varchar',
+        'length' => 50,
+        'not null' => TRUE,
+        'default' => '',
+        'binary' => TRUE,
+      ),
+      'age' => array(
+        'description' => "The person's age",
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'job' => array(
+        'description' => "The person's job",
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => 'Undefined',
+      ),
+    ),
+    'primary key' => array('name', 'age'),
+  );
+
   return $schema;
 }
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.install b/core/modules/system/tests/modules/entity_test/entity_test.install
index 9e48029..f2db594 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.install
+++ b/core/modules/system/tests/modules/entity_test/entity_test.install
@@ -36,406 +36,3 @@ function entity_test_install() {
       ->save();
   }
 }
-
-/**
- * Implements hook_schema().
- */
-function entity_test_schema() {
-  // Schema for simple entity.
-  $schema['entity_test'] = array(
-    'description' => 'Stores entity_test items.',
-    'fields' => array(
-      'id' => array(
-        'type' => 'serial',
-        'not null' => TRUE,
-        'description' => 'Primary Key: Unique entity-test item ID.',
-      ),
-      'uuid' => array(
-        'description' => 'Unique Key: Universally unique identifier for this entity.',
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => FALSE,
-      ),
-      'type' => array(
-        'description' => 'The bundle of the test entity.',
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of the original variant of this test entity.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'name' => array(
-        'description' => 'The name of the test entity.',
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'user_id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => FALSE,
-        'default' => NULL,
-        'description' => 'The {users}.uid of the associated user.',
-      ),
-    ),
-    'primary key' => array('id'),
-    'unique keys' => array(
-      'uuid' => array('uuid'),
-    ),
-  );
-
-  // Schema for entity with revisions.
-  $schema['entity_test_rev'] = array(
-    'description' => 'Stores entity_test_rev items.',
-    'fields' => array(
-      'id' => array(
-        'type' => 'serial',
-        'not null' => TRUE,
-        'description' => 'Primary Key: Unique entity-test item ID.',
-      ),
-      'revision_id' => array(
-        'description' => 'The current {entity_test_rev_property_revision}.revision_id version identifier.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'uuid' => array(
-        'description' => 'Unique Key: Universally unique identifier for this entity.',
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => FALSE,
-      ),
-      'type' => array(
-        'description' => 'The bundle of the test entity.',
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'name' => array(
-        'description' => 'The name of the test entity.',
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'user_id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => FALSE,
-        'default' => NULL,
-        'description' => 'The {users}.uid of the associated user.',
-      ),
-    ),
-    'primary key' => array('id'),
-    'unique keys' => array(
-      'uuid' => array('uuid'),
-    ),
-  );
-  $schema['entity_test_rev_revision'] = array(
-    'description' => 'Stores entity_test_rev item property revisions.',
-    'fields' => array(
-      'id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => 'The {entity_test_rev}.id of the test entity.',
-      ),
-      'revision_id' => array(
-        'type' => 'serial',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => 'The primary identifier for this version.',
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of this variant of this test entity.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'name' => array(
-        'description' => 'The name of the test entity.',
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'user_id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => FALSE,
-        'default' => NULL,
-        'description' => 'The {users}.uid of the associated user.',
-      ),
-    ),
-    'indexes' => array(
-      'user_id' => array('user_id'),
-    ),
-    'foreign keys' => array(
-      'user_id' => array('users' => 'uid'),
-      'id' => array('entity_test_rev' => 'id'),
-    ),
-    'primary key' => array('revision_id'),
-  );
-
-  // Schema for entity with data table.
-  $schema['entity_test_mul'] = array(
-    'description' => 'Stores entity_test_mul items.',
-    'fields' => array(
-      'id' => array(
-        'type' => 'serial',
-        'not null' => TRUE,
-        'description' => 'Primary Key: Unique entity-test item ID.',
-      ),
-      'uuid' => array(
-        'description' => 'Unique Key: Universally unique identifier for this entity.',
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => FALSE,
-      ),
-      'type' => array(
-        'description' => 'The bundle of the test entity.',
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of the original variant of this test entity.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-    ),
-    'primary key' => array('id'),
-    'unique keys' => array(
-      'uuid' => array('uuid'),
-    ),
-  );
-  $schema['entity_test_mul_property_data'] = array(
-    'description' => 'Stores entity_test_mul item properties.',
-    'fields' => array(
-      'id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => 'The {entity_test_mul}.id of the test entity.',
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of this variant of this test entity.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'default_langcode' => array(
-        'description' => 'Boolean indicating whether the current variant is in the original entity language.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 1,
-      ),
-      'name' => array(
-        'description' => 'The name of the test entity.',
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => FALSE,
-      ),
-      'user_id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => FALSE,
-        'default' => NULL,
-        'description' => 'The {users}.uid of the associated user.',
-      ),
-    ),
-    'indexes' => array(
-      'user_id' => array('user_id'),
-    ),
-    'foreign keys' => array(
-      'user_id' => array('users' => 'uid'),
-      'id' => array('entity_test_mul' => 'id'),
-    ),
-    'primary key' => array('id', 'langcode'),
-  );
-
-  // Schema for entity with data table and revisions.
-  $schema['entity_test_mulrev'] = array(
-    'description' => 'Stores entity_test_mulrev items.',
-    'fields' => array(
-      'id' => array(
-        'type' => 'serial',
-        'not null' => TRUE,
-        'description' => 'Primary Key: Unique entity-test item ID.',
-      ),
-      'revision_id' => array(
-        'description' => 'The current {entity_test_mulrev_property_revision}.revision_id version identifier.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'uuid' => array(
-        'description' => 'Unique Key: Universally unique identifier for this entity.',
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => FALSE,
-      ),
-      'type' => array(
-        'description' => 'The bundle of the test entity.',
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-    ),
-    'primary key' => array('id'),
-    'unique keys' => array(
-      'uuid' => array('uuid'),
-    ),
-  );
-  $schema['entity_test_mulrev_revision'] = array(
-    'description' => 'Stores entity_test_rev item property revisions.',
-    'fields' => array(
-      'id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => 'The {entity_test_rev}.id of the test entity.',
-      ),
-      'revision_id' => array(
-        'type' => 'serial',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => 'The primary identifier for this version.',
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of this variant of this test entity.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-    ),
-    'foreign keys' => array(
-      'id' => array('entity_test_rev' => 'id'),
-    ),
-    'primary key' => array('revision_id'),
-  );
-  $schema['entity_test_mulrev_property_data'] = array(
-    'description' => 'Stores entity_test_mulrev item properties.',
-    'fields' => array(
-      'id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => 'The {entity_test_mulrev}.id of the test entity.',
-      ),
-      'revision_id' => array(
-        'description' => 'The current {entity_test_mulrev_property_revision}.revision_id version identifier.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of this variant of this test entity.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'default_langcode' => array(
-        'description' => 'Boolean indicating whether the current variant is in the original entity language.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 1,
-      ),
-      'name' => array(
-        'description' => 'The name of the test entity.',
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'user_id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => FALSE,
-        'default' => NULL,
-        'description' => 'The {users}.uid of the associated user.',
-      ),
-    ),
-    'indexes' => array(
-      'user_id' => array('user_id'),
-    ),
-    'foreign keys' => array(
-      'user_id' => array('users' => 'uid'),
-      'id' => array('entity_test_mulrev' => 'id'),
-    ),
-    'primary key' => array('id', 'langcode'),
-  );
-  $schema['entity_test_mulrev_property_revision'] = array(
-    'description' => 'Stores entity_test_mulrev item property revisions.',
-    'fields' => array(
-      'id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => 'The {entity_test_mulrev}.id of the test entity.',
-      ),
-      'revision_id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => 'The primary identifier for this version.',
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of this variant of this test entity.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'default_langcode' => array(
-        'description' => 'Boolean indicating whether the current variant is in the original entity language.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 1,
-      ),
-      'name' => array(
-        'description' => 'The name of the test entity.',
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'user_id' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => FALSE,
-        'default' => NULL,
-        'description' => 'The {users}.uid of the associated user.',
-      ),
-    ),
-    'indexes' => array(
-      'user_id' => array('user_id'),
-    ),
-    'foreign keys' => array(
-      'user_id' => array('users' => 'uid'),
-      'id' => array('entity_test_mulrev' => 'id'),
-    ),
-    'primary key' => array('revision_id', 'langcode'),
-  );
-
-  return $schema;
-}
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTest.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTest.php
index a8f7191..688f1c4 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTest.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTest.php
@@ -81,7 +81,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
       ->setLabel(t('Name'))
       ->setDescription(t('The name of the test entity.'))
       ->setTranslatable(TRUE)
-      ->setSetting('max_length', 32);
+      ->setSetting('max_length', EntityTypeInterface::BUNDLE_MAX_LENGTH);
 
     // @todo: Add allowed values validation.
     $fields['type'] = FieldDefinition::create('string')
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestBaseFieldDisplay.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestBaseFieldDisplay.php
index cddccc6..324e70a 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestBaseFieldDisplay.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestBaseFieldDisplay.php
@@ -28,7 +28,6 @@
  *   entity_keys = {
  *     "id" = "id",
  *     "uuid" = "uuid",
- *     "revision" = "revision_id",
  *     "bundle" = "type"
  *   },
  *   links = {
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestLabelCallback.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestLabelCallback.php
index 481711f..e881b9a 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestLabelCallback.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestLabelCallback.php
@@ -15,7 +15,6 @@
  *   label = @Translation("Entity test label callback"),
  *   field_cache = FALSE,
  *   base_table = "entity_test",
- *   revision_table = "entity_test_revision",
  *   label_callback = "entity_test_label_callback",
  *   fieldable = TRUE,
  *   entity_keys = {
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMul.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMul.php
index c38e806..f906ccd 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMul.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMul.php
@@ -44,17 +44,4 @@
  */
 class EntityTestMul extends EntityTest {
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
-    $fields = parent::baseFieldDefinitions($entity_type);
-
-    $fields['default_langcode'] = FieldDefinition::create('boolean')
-      ->setLabel(t('Default language'))
-      ->setDescription(t('Flag to indicate whether this is the default language.'));
-
-    return $fields;
-  }
-
 }
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMulRev.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMulRev.php
index 071ed67..d64afea 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMulRev.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMulRev.php
@@ -44,23 +44,4 @@
  */
 class EntityTestMulRev extends EntityTestRev {
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
-    $fields = parent::baseFieldDefinitions($entity_type);
-
-    $fields['revision_id'] = FieldDefinition::create('integer')
-      ->setLabel(t('Revision ID'))
-      ->setDescription(t('The version id of the test entity.'))
-      ->setReadOnly(TRUE)
-      ->setSetting('unsigned', TRUE);
-
-    $fields['default_langcode'] = FieldDefinition::create('boolean')
-      ->setLabel(t('Default language'))
-      ->setDescription(t('Flag to indicate whether this is the default language.'));
-
-    return $fields;
-  }
-
 }
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TermStorage.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermStorage.php
index b7b9201..8a6385a 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/TermStorage.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermStorage.php
@@ -151,4 +151,90 @@ public function resetWeights($vid) {
       ->execute();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function buildSchema() {
+    $schema = parent::buildSchema();
+
+    $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,
+        ),
+      ),
+      'indexes' => array(
+        'term_node' => array('tid', 'sticky', 'created'),
+        'nid' => array('nid'),
+      ),
+      '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/lib/Drupal/taxonomy/Tests/TaxonomyTermReferenceItemTest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TaxonomyTermReferenceItemTest.php
index 0a67140..78c877f 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TaxonomyTermReferenceItemTest.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TaxonomyTermReferenceItemTest.php
@@ -42,8 +42,7 @@ public static function getInfo() {
 
   public function setUp() {
     parent::setUp();
-    $this->installSchema('taxonomy', 'taxonomy_term_data');
-    $this->installSchema('taxonomy', 'taxonomy_term_hierarchy');
+    $this->installEntitySchema('taxonomy_term');
 
     $vocabulary = entity_create('taxonomy_vocabulary', array(
       'name' => $this->randomName(),
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermValidationTest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermValidationTest.php
index 737f662..f545403 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermValidationTest.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermValidationTest.php
@@ -34,7 +34,7 @@ public static function getInfo() {
    */
   public function setUp() {
     parent::setUp();
-    $this->installSchema('taxonomy', array('taxonomy_term_data'));
+    $this->installEntitySchema('taxonomy_term');
   }
 
   /**
diff --git a/core/modules/taxonomy/taxonomy.install b/core/modules/taxonomy/taxonomy.install
deleted file mode 100644
index a3f7e12..0000000
--- a/core/modules/taxonomy/taxonomy.install
+++ /dev/null
@@ -1,164 +0,0 @@
-<?php
-
-/**
- * @file
- * Install, update and uninstall functions for the taxonomy module.
- */
-
-use Drupal\Core\Entity\EntityTypeInterface;
-
-/**
- * Implements hook_schema().
- */
-function taxonomy_schema() {
-  $schema['taxonomy_term_data'] = array(
-    'description' => 'Stores term information.',
-    'fields' => array(
-      'tid' => array(
-        'type' => 'serial',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => 'Primary Key: Unique term ID.',
-      ),
-      'uuid' => array(
-        'description' => 'Unique Key: Universally unique identifier for this entity.',
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => FALSE,
-      ),
-      'vid' => array(
-        'type' => 'varchar',
-        'length' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'The ID of the vocabulary to which the term is assigned.',
-      ),
-      'langcode' => array(
-        'description' => 'The {language}.langcode of this term.',
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'name' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'The term name.',
-      ),
-      'description__value' => array(
-        'type' => 'text',
-        'not null' => FALSE,
-        'size' => 'big',
-        'description' => 'A description of the term.',
-      ),
-      'description__format' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => FALSE,
-        'description' => 'The filter format ID of the description.',
-      ),
-      'weight' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'The weight of this term in relation to other terms.',
-      ),
-      'changed' => array(
-        'description' => 'The Unix timestamp when the term was most recently saved.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-    ),
-    'primary key' => array('tid'),
-    'unique keys' => array(
-      'uuid' => array('uuid'),
-    ),
-    'indexes' => array(
-      'taxonomy_tree' => array('vid', 'weight', 'name'),
-      'vid_name' => array('vid', 'name'),
-      '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,
-      ),
-    ),
-    'indexes' => array(
-      'term_node' => array('tid', 'sticky', 'created'),
-      'nid' => array('nid'),
-    ),
-    '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/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php b/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php
index 8e37920..633ee0b 100644
--- a/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php
+++ b/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php
@@ -48,7 +48,7 @@ function setUp() {
 
     // Configure the theme system.
     $this->installConfig(array('system', 'field'));
-    $this->installSchema('entity_test', 'entity_test');
+    $this->installEntitySchema('entity_test');
 
     // @todo Add helper methods for all of the following.
 
diff --git a/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php b/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php
index 9b93095..30af0bf 100644
--- a/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php
+++ b/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php
@@ -50,7 +50,7 @@ public static function getInfo() {
   public function setUp() {
     parent::setUp();
 
-    $this->installSchema('entity_test', array('entity_test_rev', 'entity_test_rev_revision'));
+    $this->installEntitySchema('entity_test_rev');
 
     // Create the necessary formats.
     $this->installConfig(array('filter'));
diff --git a/core/modules/tracker/tracker.install b/core/modules/tracker/tracker.install
index 5f3e5a0..2a72272 100644
--- a/core/modules/tracker/tracker.install
+++ b/core/modules/tracker/tracker.install
@@ -16,12 +16,18 @@ function tracker_uninstall() {
  * Implements hook_install().
  */
 function tracker_install() {
-  $max_nid = db_query('SELECT MAX(nid) FROM {node}')->fetchField();
-  if ($max_nid != 0) {
-    \Drupal::state()->set('tracker.index_nid', $max_nid);
-    // To avoid timing out while attempting to do a complete indexing, we
-    // simply call our cron job to remove stale records and begin the process.
-    tracker_cron();
+  // @todo Is there some way to avoid this check?
+  if (\Drupal::database()->schema()->tableExists('node')) {
+    $nids = \Drupal::entityQuery('node')
+      ->sort('nid', 'DESC')
+      ->range(0, 1)
+      ->execute();
+    if (!empty($nids)) {
+      \Drupal::state()->set('tracker.index_nid', reset($nids));
+      // To avoid timing out while attempting to do a complete indexing, we
+      // simply call our cron job to remove stale records and begin the process.
+      tracker_cron();
+    }
   }
 }
 
diff --git a/core/modules/user/lib/Drupal/user/AccountFormController.php b/core/modules/user/lib/Drupal/user/AccountFormController.php
index ed4cc07..d642e06 100644
--- a/core/modules/user/lib/Drupal/user/AccountFormController.php
+++ b/core/modules/user/lib/Drupal/user/AccountFormController.php
@@ -366,8 +366,8 @@ public function validate(array $form, array &$form_state) {
       // Move text value for user signature into 'signature'.
       $form_state['values']['signature'] = $form_state['values']['signature']['value'];
 
-      $user_schema = drupal_get_schema('users');
-      if (drupal_strlen($form_state['values']['signature']) > $user_schema['fields']['signature']['length']) {
+      $field_definitions = $this->entityManager->getFieldDefinitions('user', $this->getEntity()->bundle());
+      if (drupal_strlen($form_state['values']['signature']) > $field_definitions['signature']->getSetting('max_length')) {
         $this->setFormError('signature', $form_state, $this->t('The signature is too long: it must be %max characters or less.', array('%max' => $user_schema['fields']['signature']['length'])));
       }
     }
diff --git a/core/modules/user/lib/Drupal/user/Entity/User.php b/core/modules/user/lib/Drupal/user/Entity/User.php
index 8bb2f51..72957b5 100644
--- a/core/modules/user/lib/Drupal/user/Entity/User.php
+++ b/core/modules/user/lib/Drupal/user/Entity/User.php
@@ -507,6 +507,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
     $fields['status'] = FieldDefinition::create('boolean')
       ->setLabel(t('User status'))
       ->setDescription(t('Whether the user is active (1) or blocked (0).'))
+      // TODO This should be FALSE.
       ->setSetting('default_value', 1);
 
     $fields['created'] = FieldDefinition::create('created')
@@ -531,6 +532,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
     // @todo Convert this to entity_reference_field, see
     // https://drupal.org/node/2044859.
     $fields['roles'] = FieldDefinition::create('string')
+      ->setCustomStorage(TRUE)
       ->setLabel(t('Roles'))
       ->setCardinality(FieldDefinitionInterface::CARDINALITY_UNLIMITED)
       ->setDescription(t('The roles the user has.'));
diff --git a/core/modules/user/lib/Drupal/user/Tests/UserAdminTest.php b/core/modules/user/lib/Drupal/user/Tests/UserAdminTest.php
index fb58bc1..6219f89 100644
--- a/core/modules/user/lib/Drupal/user/Tests/UserAdminTest.php
+++ b/core/modules/user/lib/Drupal/user/Tests/UserAdminTest.php
@@ -31,13 +31,22 @@ public static function getInfo() {
    */
   function testUserAdmin() {
     $user_a = $this->drupalCreateUser();
+    $user_a->name = 'User A';
     $user_a->mail = $this->randomName() . '@example.com';
     $user_a->save();
     $user_b = $this->drupalCreateUser(array('administer taxonomy'));
+    $user_b->name = 'User B';
+    $user_b->save();
     $user_c = $this->drupalCreateUser(array('administer taxonomy'));
+    $user_c->name = 'User C';
+    $user_c->save();
 
     // Create admin user to delete registered user.
     $admin_user = $this->drupalCreateUser(array('administer users'));
+    // Use a predictable name so that we can reliably order the user admin page
+    // by name.
+    $admin_user->name = 'Admin user';
+    $admin_user->save();
     $this->drupalLogin($admin_user);
     $this->drupalGet('admin/people');
     $this->assertText($user_a->getUsername(), 'Found user A on admin users page');
@@ -83,8 +92,12 @@ function testUserAdmin() {
     $this->assertTrue($account->isActive(), 'User C not blocked');
     $edit = array();
     $edit['action'] = 'user_block_user_action';
-    $edit['user_bulk_form[1]'] = TRUE;
-    $this->drupalPostForm('admin/people', $edit, t('Apply'));
+    $edit['user_bulk_form[4]'] = TRUE;
+    $this->drupalPostForm('admin/people', $edit, t('Apply'), array(
+      // Sort the table by username so that we know reliably which user will be
+      // targeted with the blocking action.
+      'query' => array('order' => 'name', 'sort' => 'asc')
+    ));
     $account = user_load($user_c->id(), TRUE);
     $this->assertTrue($account->isBlocked(), 'User C blocked');
 
@@ -97,8 +110,12 @@ function testUserAdmin() {
     // Test unblocking of a user from /admin/people page and sending of activation mail
     $editunblock = array();
     $editunblock['action'] = 'user_unblock_user_action';
-    $editunblock['user_bulk_form[1]'] = TRUE;
-    $this->drupalPostForm('admin/people', $editunblock, t('Apply'));
+    $editunblock['user_bulk_form[4]'] = TRUE;
+    $this->drupalPostForm('admin/people', $editunblock, t('Apply'), array(
+      // Sort the table by username so that we know reliably which user will be
+      // targeted with the blocking action.
+      'query' => array('order' => 'name', 'sort' => 'asc')
+    ));
     $account = user_load($user_c->id(), TRUE);
     $this->assertTrue($account->isActive(), 'User C unblocked');
     $this->assertMail("to", $account->getEmail(), "Activation mail sent to user C");
diff --git a/core/modules/user/lib/Drupal/user/Tests/UserInstallTest.php b/core/modules/user/lib/Drupal/user/Tests/UserInstallTest.php
index 4e54ff8..25c61a8 100644
--- a/core/modules/user/lib/Drupal/user/Tests/UserInstallTest.php
+++ b/core/modules/user/lib/Drupal/user/Tests/UserInstallTest.php
@@ -37,7 +37,9 @@ public static function getInfo() {
    */
   protected function setUp() {
     parent::setUp();
-    $this->installSchema('user', array('users'));
+    $this->container->get('module_handler')->loadInclude('user', 'install');
+    $this->installEntitySchema('user');
+    user_entity_schema_installed(array('user' => $this->container->get('entity.manager')->getStorage('user')));
   }
 
 
@@ -45,7 +47,6 @@ protected function setUp() {
    * Test that the initial users have correct values.
    */
   public function testUserInstall() {
-    user_install();
     $anon = db_query('SELECT * FROM {users} WHERE uid = 0')->fetchObject();
     $admin = db_query('SELECT * FROM {users} WHERE uid = 1')->fetchObject();
     $this->assertFalse(empty($anon->uuid), 'Anon user has a UUID');
diff --git a/core/modules/user/lib/Drupal/user/Tests/UserValidationTest.php b/core/modules/user/lib/Drupal/user/Tests/UserValidationTest.php
index 7beec6a..2b50084 100644
--- a/core/modules/user/lib/Drupal/user/Tests/UserValidationTest.php
+++ b/core/modules/user/lib/Drupal/user/Tests/UserValidationTest.php
@@ -36,7 +36,7 @@ public static function getInfo() {
    */
   public function setUp() {
     parent::setUp();
-    $this->installSchema('user', array('users'));
+    $this->installEntitySchema('user');
     $this->installSchema('system', array('sequences'));
   }
 
diff --git a/core/modules/user/lib/Drupal/user/Tests/Views/UserUnitTestBase.php b/core/modules/user/lib/Drupal/user/Tests/Views/UserUnitTestBase.php
index eafc620..16ba123 100644
--- a/core/modules/user/lib/Drupal/user/Tests/Views/UserUnitTestBase.php
+++ b/core/modules/user/lib/Drupal/user/Tests/Views/UserUnitTestBase.php
@@ -48,7 +48,7 @@ protected function setUp() {
 
     ViewTestData::createTestViews(get_class($this), array('user_test_views'));
 
-    $this->installSchema('user', array('users', 'users_roles'));
+    $this->installEntitySchema('user');
     $this->installSchema('system', 'sequences');
 
     $entity_manager = $this->container->get('entity.manager');
diff --git a/core/modules/user/lib/Drupal/user/UserStorage.php b/core/modules/user/lib/Drupal/user/UserStorage.php
index d3f4990..a17446e 100644
--- a/core/modules/user/lib/Drupal/user/UserStorage.php
+++ b/core/modules/user/lib/Drupal/user/UserStorage.php
@@ -9,7 +9,9 @@
 
 use Drupal\Component\Uuid\UuidInterface;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandlerInterface;
 use Drupal\Core\Password\PasswordInterface;
 use Drupal\Core\Database\Connection;
 use Drupal\field\FieldInfo;
@@ -46,17 +48,20 @@ class UserStorage extends ContentEntityDatabaseStorage implements UserStorageInt
    *   The entity type definition.
    * @param \Drupal\Core\Database\Connection $database
    *   The database connection to be used.
-   * @param \Drupal\field\FieldInfo $field_info
-   *   The field info service.
-   * @param \Drupal\Component\Uuid\UuidInterface $uuid_service
-   *   The UUID Service.
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   * @param \Drupal\Core\Entity\Schema\ContentEntitySchemaHandlerInterface $schema_builder
+   *   The entity schema builder.
    * @param \Drupal\Core\Password\PasswordInterface $password
    *   The password hashing service.
    * @param \Drupal\user\UserDataInterface $user_data
    *   The user data service.
+   * @param \Drupal\field\FieldInfo $field_info
+   *   (optional) The field info service. Defaults to NULL as user storage can
+   *   be instantiated before Field module is installed.
    */
-  public function __construct(EntityTypeInterface $entity_type, Connection $database, FieldInfo $field_info, UuidInterface $uuid_service, PasswordInterface $password, UserDataInterface $user_data) {
-    parent::__construct($entity_type, $database, $field_info, $uuid_service);
+  public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, PasswordInterface $password, UserDataInterface $user_data, FieldInfo $field_info = NULL) {
+    parent::__construct($entity_type, $database, $entity_manager, $field_info);
 
     $this->password = $password;
     $this->userData = $user_data;
@@ -69,10 +74,10 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
     return new static(
       $entity_type,
       $container->get('database'),
-      $container->get('field.info'),
-      $container->get('uuid'),
+      $container->get('entity.manager'),
       $container->get('password'),
-      $container->get('user.data')
+      $container->get('user.data'),
+      $container->has('field.info') ? $container->get('field.info') : NULL
     );
   }
 
@@ -154,4 +159,93 @@ public function updateLastLoginTimestamp(UserInterface $account) {
       ->execute();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function buildSchema() {
+    $schema = parent::buildSchema();
+
+    // The "users" table does not user serial identifiers.
+    $schema['users']['fields']['uid']['type'] = 'int';
+
+    $schema['users_data'] = array(
+      'description' => 'Stores module data as key/value pairs per user.',
+      'fields' => array(
+        'uid' => array(
+          'description' => 'Primary key: {users}.uid for user.',
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+        ),
+        'module' => array(
+          'description' => 'The name of the module declaring the variable.',
+          'type' => 'varchar',
+          'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
+          'not null' => TRUE,
+          'default' => '',
+        ),
+        'name' => array(
+          'description' => 'The identifier of the data.',
+          'type' => 'varchar',
+          'length' => 128,
+          'not null' => TRUE,
+          'default' => '',
+        ),
+        'value' => array(
+          'description' => 'The value.',
+          'type' => 'blob',
+          'not null' => FALSE,
+          'size' => 'big',
+        ),
+        'serialized' => array(
+          'description' => 'Whether value is serialized.',
+          'type' => 'int',
+          'size' => 'tiny',
+          'unsigned' => TRUE,
+          'default' => 0,
+        ),
+      ),
+      'primary key' => array('uid', 'module', 'name'),
+      'indexes' => array(
+        'module' => array('module'),
+        'name' => array('name'),
+      ),
+      'foreign keys' => array(
+        'uid' => array('users' => 'uid'),
+      ),
+    );
+
+    $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/tests/modules/user_test_views/test_views/views.view.test_filter_permission.yml b/core/modules/user/tests/modules/user_test_views/test_views/views.view.test_filter_permission.yml
index b16cb3e..8b62fc7 100644
--- a/core/modules/user/tests/modules/user_test_views/test_views/views.view.test_filter_permission.yml
+++ b/core/modules/user/tests/modules/user_test_views/test_views/views.view.test_filter_permission.yml
@@ -115,7 +115,20 @@ display:
             group_items: {  }
           reduce_duplicates: '1'
           plugin_id: user_permissions
-      sorts: {  }
+      sorts:
+        uid:
+          id: uid
+          table: users
+          field: uid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          order: ASC
+          exposed: false
+          expose:
+            label: ''
+          plugin_id: standard
+          provider: views
 human_name: test_filter_permission
 module: views
 id: test_filter_permission
diff --git a/core/modules/user/user.info.yml b/core/modules/user/user.info.yml
index 45a421a..dbdb520 100644
--- a/core/modules/user/user.info.yml
+++ b/core/modules/user/user.info.yml
@@ -6,3 +6,5 @@ version: VERSION
 core: 8.x
 required: true
 configure: user.admin_index
+dependencies:
+  - entity
diff --git a/core/modules/user/user.install b/core/modules/user/user.install
index 97515a2..e5b9161 100644
--- a/core/modules/user/user.install
+++ b/core/modules/user/user.install
@@ -5,243 +5,36 @@
  * Install, update and uninstall functions for the user module.
  */
 
-use Drupal\Core\Language\Language;
-use Drupal\field\Field;
+use Drupal\Core\Entity\ContentEntityDatabaseStorage;
 
 /**
- * Implements hook_schema().
+ * Implements hook_entity_schema_installed().
  */
-function user_schema() {
-  // The table name here is plural, despite Drupal table naming standards,
-  // because "user" is a reserved word in many databases.
-  $schema['users'] = array(
-    'description' => 'Stores user data.',
-    'fields' => array(
-      'uid' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => 'Primary Key: Unique user ID.',
-        'default' => 0,
-      ),
-      'uuid' => array(
-        'description' => 'Unique Key: Universally unique identifier for this entity.',
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => FALSE,
-      ),
-      'name' => array(
-        'type' => 'varchar',
-        'length' => 60,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'Unique user name.',
-      ),
-      'langcode' => array(
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => "The {language}.langcode of the user's profile.",
-      ),
-      'pass' => array(
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => "User's password (hashed).",
-      ),
-      'mail' => array(
-        'type' => 'varchar',
-        'length' => 254,
-        'not null' => FALSE,
-        'default' => '',
-        'description' => "User's e-mail address.",
-      ),
-      'signature' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => "User's signature.",
-      ),
-      'signature_format' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => FALSE,
-        'description' => 'The filter format ID of the signature.',
-      ),
-      'created' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'Timestamp for when user was created.',
-      ),
-      'access' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'Timestamp for previous time user accessed the site.',
-      ),
-      'login' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => "Timestamp for user's last login.",
-      ),
-      'status' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'size' => 'tiny',
-        'description' => 'Whether the user is active(1) or blocked(0).',
-      ),
-      'timezone' => array(
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => FALSE,
-        'description' => "User's time zone.",
-      ),
-      'preferred_langcode' => array(
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'The {language}.langcode that the user prefers for receiving emails and viewing the site.',
-      ),
-      'preferred_admin_langcode' => array(
-        'type' => 'varchar',
-        'length' => 12,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'The {language}.langcode that the user prefers for viewing administration pages.',
-      ),
-      'init' => array(
-        'type' => 'varchar',
-        'length' => 254,
-        'not null' => FALSE,
-        'default' => '',
-        'description' => 'E-mail address used for initial account creation.',
-      ),
-    ),
-    'indexes' => array(
-      'access' => array('access'),
-      'created' => array('created'),
-      'mail' => array('mail'),
-    ),
-    'unique keys' => array(
-      'uuid' => array('uuid'),
-      'name' => array('name'),
-    ),
-    'primary key' => array('uid'),
-  );
-
-  $schema['users_data'] = array(
-    'description' => 'Stores module data as key/value pairs per user.',
-    'fields' => array(
-      'uid' => array(
-        'description' => 'Primary key: {users}.uid for user.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'module' => array(
-        'description' => 'The name of the module declaring the variable.',
-        'type' => 'varchar',
-        'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'name' => array(
-        'description' => 'The identifier of the data.',
-        'type' => 'varchar',
-        'length' => 128,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'value' => array(
-        'description' => 'The value.',
-        'type' => 'blob',
-        'not null' => FALSE,
-        'size' => 'big',
-      ),
-      'serialized' => array(
-        'description' => 'Whether value is serialized.',
-        'type' => 'int',
-        'size' => 'tiny',
-        'unsigned' => TRUE,
-        'default' => 0,
-      ),
-    ),
-    'primary key' => array('uid', 'module', 'name'),
-    'indexes' => array(
-      'module' => array('module'),
-      'name' => array('name'),
-    ),
-    'foreign keys' => array(
-      'uid' => array('users' => 'uid'),
-    ),
-  );
-
-  $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;
-}
-
-/**
- * Implements hook_install().
- */
-function user_install() {
-  // Insert a row for the anonymous user.
-  db_insert('users')
-    ->fields(array(
-      'uid' => 0,
-      'uuid' => \Drupal::service('uuid')->generate(),
-      'name' => '',
-      'mail' => '',
-      'langcode' => \Drupal::languageManager()->getDefaultLanguage()->id,
-    ))
-    ->execute();
-
-  // We need some placeholders here as name and mail are uniques.
-  // This will be changed by the settings form in the installer.
-  db_insert('users')
-    ->fields(array(
-      'uid' => 1,
-      'uuid' => \Drupal::service('uuid')->generate(),
-      'name' => 'placeholder-for-uid-1',
-      'mail' => 'placeholder-for-uid-1',
-      'created' => REQUEST_TIME,
-      'status' => 1,
-      'langcode' => \Drupal::languageManager()->getDefaultLanguage()->id,
-    ))
-    ->execute();
+function user_entity_schema_installed(array $storages) {
+  if (isset($storages['user'])) {
+    // Insert a row for the anonymous user.
+    db_insert('users')
+      ->fields(array(
+        'uid' => 0,
+        'uuid' => \Drupal::service('uuid')->generate(),
+        'name' => '',
+        'mail' => '',
+        'langcode' => \Drupal::languageManager()->getDefaultLanguage()->id,
+        ))
+      ->execute();
+
+    // We need some placeholders here as name and mail are uniques.
+    // This will be changed by the settings form in the installer.
+    db_insert('users')
+      ->fields(array(
+        'uid' => 1,
+        'uuid' => \Drupal::service('uuid')->generate(),
+        'name' => 'placeholder-for-uid-1',
+        'mail' => 'placeholder-for-uid-1',
+        'created' => REQUEST_TIME,
+        'status' => 1,
+        'langcode' => \Drupal::languageManager()->getDefaultLanguage()->id,
+      ))
+      ->execute();
+  }
 }
diff --git a/core/modules/views/lib/Drupal/views/Entity/View.php b/core/modules/views/lib/Drupal/views/Entity/View.php
index 2beb7ac..54ab465 100644
--- a/core/modules/views/lib/Drupal/views/Entity/View.php
+++ b/core/modules/views/lib/Drupal/views/Entity/View.php
@@ -277,7 +277,10 @@ public function calculateDependencies() {
     // Ensure that the view is dependant on the module that provides the schema
     // for the base table.
     $schema = drupal_get_schema($this->base_table);
-    if ($this->module != $schema['module']) {
+    // @todo Entity base tables are no longer registered in hook_schema(). Once
+    //   we automate the views data for entity types add the entity type
+    //   type provider as a dependency.
+    if ($schema && $this->module != $schema['module']) {
       $this->addDependency('module', $schema['module']);
     }
 
diff --git a/core/modules/views/lib/Drupal/views/Tests/Entity/RowEntityRenderersTest.php b/core/modules/views/lib/Drupal/views/Tests/Entity/RowEntityRenderersTest.php
index 1694fd0..18ddff0 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Entity/RowEntityRenderersTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Entity/RowEntityRenderersTest.php
@@ -56,8 +56,8 @@ public static function getInfo() {
   protected function setUp() {
     parent::setUp();
 
-    $this->installSchema('node', array('node', 'node_revision', 'node_field_data', 'node_field_revision', 'node_access'));
-    $this->installSchema('user', array('users'));
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
     $this->installConfig(array('node', 'language'));
 
     $this->langcodes = array(\Drupal::languageManager()->getDefaultLanguage()->id);
diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/AreaTextTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/AreaTextTest.php
index 2ac78f8..3ab29d7 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Handler/AreaTextTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Handler/AreaTextTest.php
@@ -38,7 +38,7 @@ protected function setUp() {
     parent::setUp();
 
     $this->installConfig(array('system', 'filter'));
-    $this->installSchema('user', array('users'));
+    $this->installEntitySchema('user');
   }
 
   public function testAreaText() {
diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerAliasTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerAliasTest.php
index caec4c7..ac9b920 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerAliasTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerAliasTest.php
@@ -35,7 +35,7 @@ public static function getInfo() {
   protected function setUp() {
     parent::setUp();
 
-    $this->installSchema('user', 'users');
+    $this->installEntitySchema('user');
   }
 
   /**
diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/RelationshipJoinTestBase.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/RelationshipJoinTestBase.php
index fae3515..5542619 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Plugin/RelationshipJoinTestBase.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/RelationshipJoinTestBase.php
@@ -26,7 +26,7 @@
    * Overrides \Drupal\views\Tests\ViewUnitTestBase::setUpFixtures().
    */
   protected function setUpFixtures() {
-    $this->installSchema('user', array('users', 'users_roles'));
+    $this->installEntitySchema('user');
     $this->installConfig(array('user'));
     parent::setUpFixtures();
 
diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/RowEntityTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/RowEntityTest.php
index 0b406f5..8cecbd1 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Plugin/RowEntityTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/RowEntityTest.php
@@ -53,7 +53,7 @@ public static function getInfo() {
   protected function setUp() {
     parent::setUp();
 
-    $this->installSchema('taxonomy', array('taxonomy_term_data', 'taxonomy_term_hierarchy'));
+    $this->installEntitySchema('taxonomy_term');
     $this->installConfig(array('taxonomy'));
     \Drupal::service('router.builder')->rebuild();
   }
diff --git a/core/modules/views/lib/Drupal/views/Tests/QueryGroupByTest.php b/core/modules/views/lib/Drupal/views/Tests/QueryGroupByTest.php
index 7b7d0b7..ff0c225 100644
--- a/core/modules/views/lib/Drupal/views/Tests/QueryGroupByTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/QueryGroupByTest.php
@@ -49,7 +49,7 @@ public static function getInfo() {
   protected function setUp() {
     parent::setUp();
 
-    $this->installSchema('entity_test', array('entity_test'));
+    $this->installEntitySchema('entity_test');
 
     $this->storage = $this->container->get('entity.manager')->getStorage('entity_test');
   }
diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewExecutableTest.php b/core/modules/views/lib/Drupal/views/Tests/ViewExecutableTest.php
index c0228ba..4e2715d 100644
--- a/core/modules/views/lib/Drupal/views/Tests/ViewExecutableTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/ViewExecutableTest.php
@@ -84,10 +84,11 @@ public static function getInfo() {
   }
 
   protected function setUpFixtures() {
-    $this->installSchema('user', array('users'));
-    $this->installSchema('node', array('node', 'node_field_data'));
-    $this->installSchema('comment', array('comment', 'comment_entity_statistics'));
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('comment');
     $this->installConfig(array('field'));
+
     entity_create('node_type', array(
       'type' => 'page',
       'name' => 'Page',
diff --git a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php
new file mode 100644
index 0000000..d206fa8
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php
@@ -0,0 +1,222 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Entity\Schema\ContentEntitySchemaHandlerTest.
+ */
+
+namespace Drupal\Tests\Core\Entity\Schema;
+
+use Drupal\Core\Entity\ContentEntityDatabaseStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the content entity schema handler..
+ *
+ * @coversDefaultClass \Drupal\Core\Entity\Schema\ContentEntitySchemaHandler
+ *
+ * @group Drupal
+ * @group Entity
+ */
+class ContentEntitySchemaHandlerTest extends UnitTestCase {
+
+  /**
+   * The mocked entity manager used in this test.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $entityManager;
+
+  /**
+   * The mocked entity type used in this test.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityTypeInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $entityType;
+
+  /**
+   * The mocked SQL storage used in this test.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityDatabaseStorageInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $sqlStorage;
+
+  /**
+   * The mocked field definitions used in this test.
+   *
+   * @var \Drupal\Core\Field\FieldDefinitionInterface[]|\PHPUnit_Framework_MockObject_MockObject[]
+   */
+  protected $fieldDefinitions;
+
+  /**
+   * The content entity schema handler used in this test.
+   *
+   * @var \Drupal\Core\Entity\Schema\ContentEntitySchemaHandler.
+   */
+  protected $schemaHandler;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Content entity schema handler',
+      'description' => 'Tests the schema generation for content entities.',
+      'group' => 'Entity',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
+    $this->entityType = $this->getMock('Drupal\Core\Entity\ContentEntityTypeInterface');
+    $this->sqlStorage = $this->getMock('Drupal\Core\Entity\ContentEntityDatabaseStorageInterface');
+
+    // Set up basic expectations that apply to all tests.
+    $this->entityType->expects($this->any())
+      ->method('id')
+      ->will($this->returnValue('entity_test'));
+  }
+
+  /**
+   * Tests ContentEntitySchemaHandler::getSchema() with a basic table layout.
+   *
+   * This tests that the schema is generated correctly for non-revisionable,
+   * non-translatable entities.
+   *
+   * @covers ::getSchema
+   */
+  public function testGetSchemaLayoutBase() {
+    $this->sqlStorage->expects($this->once())
+      ->method('getLayoutType')
+      ->will($this->returnValue(ContentEntityDatabaseStorageInterface::LAYOUT_BASE));
+    $this->sqlStorage->expects($this->once())
+      ->method('getBaseTable')
+      ->will($this->returnValue('entity_test'));
+    // @see \Drupal\Core\Field\Plugin\Field\FieldType\IntegerItem::schema()
+    $this->setupFieldDefinition('id', array(
+      'columns' => array(
+        'value' => array(
+          'type' => 'int',
+        ),
+      ),
+    ));
+    // @see \Drupal\Core\Field\Plugin\Field\FieldType\StringItem::schema()
+    $this->setupFieldDefinition('name', array(
+      'columns' => array(
+        'value' => array(
+          'type' => 'varchar',
+          'length' => 255,
+        ),
+      ),
+    ));
+    // @see \Drupal\Core\Field\Plugin\Field\FieldType\StringItem::schema()
+    $this->setupFieldDefinition('type', array(
+      'columns' => array(
+        'value' => array(
+          'type' => 'varchar',
+          'length' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
+        ),
+      ),
+    ));
+
+    $this->entityType->expects($this->any())
+      ->method('getKey')
+      ->will($this->returnValueMap(array(
+        array('id', 'id'),
+        array('uuid', NULL),
+      )));
+
+    // EntityTypeInterface::id() is called from __construct(), the top of
+    // getSchema(), initializeBaseTable() and twice from the bottom of
+    // getSchema().
+    $this->setupSchemaHandler();
+
+    $this->sqlStorage->expects($this->once())
+      ->method('getTableMapping')
+      ->will($this->returnValue(
+        array(
+          'entity_test' => array(
+            'id' => array('id'),
+            'name' => array('name'),
+            'type' => array('type'),
+          ),
+        )
+      ));
+
+    $expected = array(
+      'entity_test' => array(
+        'description' => 'The base table for entity_test entities.',
+        'fields' => array(
+          'id' => array(
+            'type' => 'serial',
+            'description' => 'The id field.',
+            'not null' => TRUE,
+          ),
+          'name' => array(
+            'type' => 'varchar',
+            'length' => 255,
+            'description' => 'The name field.',
+            'not null' => FALSE,
+          ),
+          'type' => array(
+            'type' => 'varchar',
+            'length' => 32,
+            'description' => 'The type field.',
+            'not null' => FALSE,
+          ),
+        ),
+        'primary key' => array('id'),
+      ),
+    );
+    $actual = $this->schemaHandler->getSchema();
+
+    $this->assertEquals($expected, $actual);
+  }
+
+  /**
+   * Sets up the schema handler.
+   *
+   * This uses the field definitions set in $this->fieldDefinitions.
+   */
+  public function setupSchemaHandler() {
+    $this->entityManager->expects($this->once())
+      ->method('getBaseFieldDefinitions')
+      ->with('entity_test')
+      ->will($this->returnValue($this->fieldDefinitions));
+    $this->schemaHandler = new ContentEntitySchemaHandler(
+      $this->entityManager,
+      $this->entityType,
+      $this->sqlStorage
+    );
+  }
+
+  /**
+   * Sets up a field definition.
+   *
+   * @param string $field_name
+   *   The field name.
+   * @param array $schema
+   *   The schema array of the field definition, as returned from
+   *   FieldDefinitionInterface::schema().
+   */
+  public function setupFieldDefinition($field_name, array $schema) {
+    $this->fieldDefinitions[$field_name] = $this->getMock('Drupal\Core\Field\FieldDefinitionInterface');
+    $this->fieldDefinitions[$field_name]->expects($this->once())
+      ->method('getName')
+      ->will($this->returnValue($field_name));
+    $this->fieldDefinitions[$field_name]->expects($this->once())
+      ->method('getDescription')
+      ->will($this->returnValue("The $field_name field."));
+    // @todo ContentEntitySchemaHandler should be fixed to only call getSchema()
+    //   once per field definition.
+    $this->fieldDefinitions[$field_name]->expects($this->exactly(2))
+      ->method('getSchema')
+      ->will($this->returnValue($schema));
+  }
+
+}
+
