diff --git a/core/core.services.yml b/core/core.services.yml index 183f7e2..9f1208d 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -280,6 +280,9 @@ services: parent: container.trait tags: - { name: plugin_manager_cache_clear } + entity.schema.manager: + class: Drupal\Core\Entity\Schema\ContentEntitySchemaManager + arguments: ['@entity.manager', '@state'] entity.form_builder: class: Drupal\Core\Entity\EntityFormBuilder arguments: ['@entity.manager', '@form_builder'] diff --git a/core/includes/update.inc b/core/includes/update.inc index 945ffe7..e633f8b 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -13,6 +13,7 @@ use Drupal\Core\Config\FileStorage; use Drupal\Core\Config\ConfigException; use Drupal\Core\DrupalKernel; +use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Page\DefaultHtmlPageRenderer; use Drupal\Core\Utility\Error; use Drupal\Component\Uuid\Uuid; @@ -278,6 +279,33 @@ function update_do_one($module, $number, $dependency_map, &$context) { } /** + * Performs entity schema updates. + * + * @param $module + * The module whose update will be run. + * @param $number + * The update number to run. + * @param $context + * The batch context array. + */ +function update_entity_schema($module, $number, &$context) { + try { + \Drupal::service('entity.schema.manager')->applyChanges(); + } + catch (EntityStorageException $e) { + watchdog_exception('update', $e); + $variables = Error::decodeException($e); + unset($variables['backtrace']); + // The exception message is run through + // \Drupal\Component\Utility\String::checkPlain() by + // \Drupal\Core\Utility\Error::decodeException(). + $ret['#abort'] = array('success' => FALSE, 'query' => t('%type: !message in %function (line %line of %file).', $variables)); + $context['results'][$module][$number] = $ret; + $context['results']['#abort'][] = 'update_entity_schema'; + } +} + +/** * Starts the database update batch process. * * @param $start @@ -312,6 +340,13 @@ function update_batch($start, $redirect = NULL, $url = NULL, $batch = array(), $ } } + // First of all perform entity schema updates, if needed, so that subsequent + // updates work with a correct entity schema. + $operations = array(); + if (\Drupal::service('entity.schema.manager')->getChangeList()) { + $operations[] = array('update_entity_schema', array('system', '0 - Update entity schema')); + } + // Resolve any update dependencies to determine the actual updates that will // be run and the order they will be run in. $updates = update_resolve_dependencies($start); @@ -325,7 +360,7 @@ function update_batch($start, $redirect = NULL, $url = NULL, $batch = array(), $ $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : array(); } - $operations = array(); + // Determine updates to be performed. foreach ($updates as $update) { if ($update['allowed']) { // Set the installed version of each module so updates will start at the @@ -556,15 +591,17 @@ function update_get_update_function_list($starting_updates) { // Go through each module and find all updates that we need (including the // first update that was requested and any updates that run after it). $update_functions = array(); - foreach ($starting_updates as $module => $version) { - $update_functions[$module] = array(); - $updates = drupal_get_schema_versions($module); - if ($updates !== FALSE) { - $max_version = max($updates); - if ($version <= $max_version) { - foreach ($updates as $update) { - if ($update >= $version) { - $update_functions[$module][$update] = $module . '_update_' . $update; + if ($starting_updates) { + foreach ($starting_updates as $module => $version) { + $update_functions[$module] = array(); + $updates = drupal_get_schema_versions($module); + if ($updates !== FALSE) { + $max_version = max($updates); + if ($version <= $max_version) { + foreach ($updates as $update) { + if ($update >= $version) { + $update_functions[$module][$update] = $module . '_update_' . $update; + } } } } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php index f68a0da..395129c 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php @@ -11,15 +11,14 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Database\Database; -use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler; +use Drupal\Core\Entity\Schema\ContentEntitySchemaProviderInterface; use Drupal\Core\Entity\Sql\DefaultTableMapping; use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Language\LanguageInterface; -use Drupal\field\Entity\FieldStorageConfig; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -30,13 +29,12 @@ * * The class uses \Drupal\Core\Entity\Schema\ContentEntitySchemaHandler * internally in order to automatically generate the database schema based on - * the defined base fields. Entity types can override - * ContentEntityDatabaseStorage::getSchema() to customize the generated - * schema; e.g., to add additional indexes. + * the defined base fields. Entity types can override the schema handler to + * customize the generated schema; e.g., to add additional indexes. * * @ingroup entity_api */ -class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface { +class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, ContentEntitySchemaProviderInterface { /** * The mapping of field columns to SQL tables. @@ -106,7 +104,7 @@ class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements S /** * The entity schema handler. * - * @var \Drupal\Core\Entity\Schema\EntitySchemaHandlerInterface + * @var \Drupal\Core\Entity\Schema\ContentEntitySchemaHandlerInterface */ protected $schemaHandler; @@ -154,15 +152,40 @@ public function getFieldStorageDefinitions() { */ public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache) { parent::__construct($entity_type); - $this->database = $database; $this->entityManager = $entity_manager; $this->cacheBackend = $cache; + $this->initTableLayout(); + } + + /** + * {@inheritdoc} + */ + public function hasData() { + // We cannot use an entity query as it relies on the entity type definition, + // which is not available while updating the entity schema. + return (bool) $this->database->select($this->baseTable) + ->countQuery() + ->range(0, 1) + ->execute() + ->fetchField(); + } + + /** + * Initializes table name variables. + */ + protected function initTableLayout() { + // Reset table field values to ensure changes in the entity type definition + // are correctly reflected in the table layout. + $this->tableMapping = NULL; + $this->revisionKey = NULL; + $this->revisionTable = NULL; + $this->dataTable = NULL; + $this->revisionDataTable = NULL; // @todo Remove table names from the entity type definition in // https://drupal.org/node/2232465 $this->baseTable = $this->entityType->getBaseTable() ?: $this->entityTypeId; - $revisionable = $this->entityType->isRevisionable(); if ($revisionable) { $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id'; @@ -220,13 +243,6 @@ public function getRevisionDataTable() { } /** - * {@inheritdoc} - */ - public function getSchema() { - return $this->schemaHandler()->getSchema(); - } - - /** * Gets the schema handler for this entity storage. * * @return \Drupal\Core\Entity\Schema\ContentEntitySchemaHandler @@ -234,24 +250,44 @@ public function getSchema() { */ protected function schemaHandler() { if (!isset($this->schemaHandler)) { - $this->schemaHandler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this); + $this->schemaHandler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this, $this->database); } return $this->schemaHandler; } /** + * Updates the wrapped entity type definition. + * + * @param ContentEntityTypeInterface $entity_type + * The update entity type. + * + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. + * See https://www.drupal.org/node/2274017. + */ + public function setEntityType(ContentEntityTypeInterface $entity_type) { + if ($this->entityType->id() == $entity_type->id()) { + $this->entityType = $entity_type; + $this->initTableLayout(); + } + else { + throw new EntityStorageException(String::format('Unsupported entity type @id', array('@id' => $entity_type->id()))); + } + } + + /** * {@inheritdoc} */ - public function getTableMapping() { - if (!isset($this->tableMapping)) { + public function getTableMapping(array $storage_definitions = NULL) { + $table_mapping = $this->tableMapping; + + if (!isset($this->tableMapping) || $storage_definitions) { + $definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId); + $base_field_definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId); + $table_mapping = new DefaultTableMapping($definitions, $base_field_definitions); - $definitions = array_filter($this->getFieldStorageDefinitions(), function (FieldStorageDefinitionInterface $definition) { - // @todo Remove the check for FieldDefinitionInterface::isMultiple() when - // multiple-value base fields are supported in - // https://drupal.org/node/2248977. - return !$definition->hasCustomStorage() && !$definition->isMultiple(); + $definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) { + return $table_mapping->allowsSharedTableStorage($definition); }); - $this->tableMapping = new DefaultTableMapping($definitions); $key_fields = array_values(array_filter(array($this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey))); $all_fields = array_keys($definitions); @@ -279,16 +315,16 @@ public function getTableMapping() { $translatable = $this->entityType->getDataTable() && $this->entityType->isTranslatable(); if (!$revisionable && !$translatable) { // The base layout stores all the base field values in the base table. - $this->tableMapping->setFieldNames($this->baseTable, $all_fields); + $table_mapping->setFieldNames($this->baseTable, $all_fields); } elseif ($revisionable && !$translatable) { // The revisionable layout stores all the base field values in the base // table, except for revision metadata fields. Revisionable fields // denormalized in the base table but also stored in the revision table // together with the entity ID and the revision ID as identifiers. - $this->tableMapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields)); + $table_mapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields)); $revision_key_fields = array($this->idKey, $this->revisionKey); - $this->tableMapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields)); + $table_mapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields)); } elseif (!$revisionable && $translatable) { // Multilingual layouts store key field values in the base table. The @@ -297,7 +333,7 @@ public function getTableMapping() { // denormalized copy of the bundle field value to allow for more // performant queries. This means that only the UUID is not stored on // the data table. - $this->tableMapping + $table_mapping ->setFieldNames($this->baseTable, $key_fields) ->setFieldNames($this->dataTable, array_values(array_diff($all_fields, array($this->uuidKey)))) // Add the denormalized 'default_langcode' field to the mapping. Its @@ -313,13 +349,13 @@ public function getTableMapping() { // holds the data field values for all non-revisionable fields. The data // field values of revisionable fields are denormalized in the data // table, as well. - $this->tableMapping->setFieldNames($this->baseTable, array_values(array_diff($key_fields, array($this->langcodeKey)))); + $table_mapping->setFieldNames($this->baseTable, array_values(array_diff($key_fields, array($this->langcodeKey)))); // Like in the multilingual, non-revisionable case the UUID is not // in the data table. Additionally, do not store revision metadata // fields in the data table. $data_fields = array_values(array_diff($all_fields, array($this->uuidKey), $revision_metadata_fields)); - $this->tableMapping + $table_mapping ->setFieldNames($this->dataTable, $data_fields) // Add the denormalized 'default_langcode' field to the mapping. Its // value is identical to the query expression @@ -328,20 +364,43 @@ public function getTableMapping() { ->setExtraColumns($this->dataTable, array('default_langcode')); $revision_base_fields = array_merge(array($this->idKey, $this->revisionKey, $this->langcodeKey), $revision_metadata_fields); - $this->tableMapping->setFieldNames($this->revisionTable, $revision_base_fields); + $table_mapping->setFieldNames($this->revisionTable, $revision_base_fields); $revision_data_key_fields = array($this->idKey, $this->revisionKey, $this->langcodeKey); - $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields); - $this->tableMapping + $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, array($this->langcodeKey)); + $table_mapping ->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields)) // Add the denormalized 'default_langcode' field to the mapping. Its // value is identical to the query expression // "revision_table.langcode = data_table.langcode". ->setExtraColumns($this->revisionDataTable, array('default_langcode')); } + + // Add dedicated tables. + $definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) { + return $table_mapping->requiresDedicatedTableStorage($definition); + }); + $extra_columns = array( + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'langcode', + 'delta', + ); + foreach ($definitions as $field_name => $definition) { + foreach (array($table_mapping->getDedicatedDataTableName($definition), $table_mapping->getDedicatedRevisionTableName($definition)) as $table_name) { + $table_mapping->setFieldNames($table_name, array($field_name)); + $table_mapping->setExtraColumns($table_name, $extra_columns); + } + } + + if (!$storage_definitions) { + $this->tableMapping = $table_mapping; + } } - return $this->tableMapping; + return $table_mapping; } /** @@ -588,7 +647,7 @@ protected function attachPropertyData(array &$entities) { $table_mapping = $this->getTableMapping(); $translations = array(); if ($this->revisionDataTable) { - $data_fields = array_diff_key($table_mapping->getFieldNames($this->revisionDataTable), $table_mapping->getFieldNames($this->baseTable)); + $data_fields = array_diff($table_mapping->getFieldNames($this->revisionDataTable), $table_mapping->getFieldNames($this->baseTable)); } else { $data_fields = $table_mapping->getFieldNames($this->dataTable); @@ -1152,11 +1211,12 @@ protected function loadFieldItems(array $entities) { // Collect impacted fields. $storage_definitions = array(); $definitions = array(); + $table_mapping = $this->getTableMapping(); foreach ($bundles as $bundle => $v) { $definitions[$bundle] = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle); foreach ($definitions[$bundle] as $field_name => $field_definition) { $storage_definition = $field_definition->getFieldStorageDefinition(); - if ($this->usesDedicatedTable($storage_definition)) { + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { $storage_definitions[$field_name] = $storage_definition; } } @@ -1165,7 +1225,7 @@ protected function loadFieldItems(array $entities) { // Load field data. $langcodes = array_keys(language_list(LanguageInterface::STATE_ALL)); foreach ($storage_definitions as $field_name => $storage_definition) { - $table = $load_current ? static::_fieldTableName($storage_definition) : static::_fieldRevisionTableName($storage_definition); + $table = $load_current ? $table_mapping->getDedicatedDataTableName($storage_definition) : $table_mapping->getDedicatedRevisionTableName($storage_definition); // Ensure that only values having valid languages are retrieved. Since we // are loading values for multiple entities, we cannot limit the query to @@ -1194,7 +1254,7 @@ protected function loadFieldItems(array $entities) { // For each column declared by the field, populate the item from the // prefixed database column. foreach ($storage_definition->getColumns() as $column => $attributes) { - $column_name = static::_fieldColumnName($storage_definition, $column); + $column_name = $table_mapping->getFieldColumnName($storage_definition, $column); // Unserialize the value if specified in the column schema. $item[$column] = (!empty($attributes['serialize'])) ? unserialize($row->$column_name) : $row->$column_name; } @@ -1223,6 +1283,7 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) { $entity_type = $entity->getEntityTypeId(); $default_langcode = $entity->getUntranslated()->language()->id; $translation_langcodes = array_keys($entity->getTranslationLanguages()); + $table_mapping = $this->getTableMapping(); if (!isset($vid)) { $vid = $id; @@ -1230,11 +1291,11 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) { foreach ($this->entityManager->getFieldDefinitions($entity_type, $bundle) as $field_name => $field_definition) { $storage_definition = $field_definition->getFieldStorageDefinition(); - if (!$this->usesDedicatedTable($storage_definition)) { + if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) { continue; } - $table_name = static::_fieldTableName($storage_definition); - $revision_name = static::_fieldRevisionTableName($storage_definition); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition); // Delete and insert, rather than update, in case a value was added. if ($update) { @@ -1245,20 +1306,24 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) { ->condition('entity_id', $id) ->execute(); } - $this->database->delete($revision_name) - ->condition('entity_id', $id) - ->condition('revision_id', $vid) - ->execute(); + if ($this->entityType->isRevisionable()) { + $this->database->delete($revision_name) + ->condition('entity_id', $id) + ->condition('revision_id', $vid) + ->execute(); + } } // Prepare the multi-insert query. $do_insert = FALSE; $columns = array('entity_id', 'revision_id', 'bundle', 'delta', 'langcode'); foreach ($storage_definition->getColumns() as $column => $attributes) { - $columns[] = static::_fieldColumnName($storage_definition, $column); + $columns[] = $table_mapping->getFieldColumnName($storage_definition, $column); } $query = $this->database->insert($table_name)->fields($columns); - $revision_query = $this->database->insert($revision_name)->fields($columns); + if ($this->entityType->isRevisionable()) { + $revision_query = $this->database->insert($revision_name)->fields($columns); + } $langcodes = $field_definition->isTranslatable() ? $translation_langcodes : array($default_langcode); foreach ($langcodes as $langcode) { @@ -1276,12 +1341,14 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) { 'langcode' => $langcode, ); foreach ($storage_definition->getColumns() as $column => $attributes) { - $column_name = static::_fieldColumnName($storage_definition, $column); + $column_name = $table_mapping->getFieldColumnName($storage_definition, $column); // Serialize the value if specified in the column schema. $record[$column_name] = !empty($attributes['serialize']) ? serialize($item->$column) : $item->$column; } $query->values($record); - $revision_query->values($record); + if ($this->entityType->isRevisionable()) { + $revision_query->values($record); + } if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) { break; @@ -1296,7 +1363,9 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) { if ($entity->isDefaultRevision()) { $query->execute(); } - $revision_query->execute(); + if ($this->entityType->isRevisionable()) { + $revision_query->execute(); + } } } } @@ -1308,19 +1377,22 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) { * The entity. */ protected function deleteFieldItems(EntityInterface $entity) { + $table_mapping = $this->getTableMapping(); foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) { $storage_definition = $field_definition->getFieldStorageDefinition(); - if (!$this->usesDedicatedTable($storage_definition)) { + if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) { continue; } - $table_name = static::_fieldTableName($storage_definition); - $revision_name = static::_fieldRevisionTableName($storage_definition); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition); $this->database->delete($table_name) ->condition('entity_id', $entity->id()) ->execute(); - $this->database->delete($revision_name) - ->condition('entity_id', $entity->id()) - ->execute(); + if ($this->entityType->isRevisionable()) { + $this->database->delete($revision_name) + ->condition('entity_id', $entity->id()) + ->execute(); + } } } @@ -1332,13 +1404,14 @@ protected function deleteFieldItems(EntityInterface $entity) { */ protected function deleteFieldItemsRevision(EntityInterface $entity) { $vid = $entity->getRevisionId(); - if (isset($vid)) { + if (isset($vid) && $this->entityType->isRevisionable()) { + $table_mapping = $this->getTableMapping(); foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) { $storage_definition = $field_definition->getFieldStorageDefinition(); - if (!$this->usesDedicatedTable($storage_definition)) { + if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) { continue; } - $revision_name = static::_fieldRevisionTableName($storage_definition); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition); $this->database->delete($revision_name) ->condition('entity_id', $entity->id()) ->condition('revision_id', $vid) @@ -1348,151 +1421,127 @@ protected function deleteFieldItemsRevision(EntityInterface $entity) { } /** - * Returns whether the field uses a dedicated table for storage. - * - * @param FieldStorageDefinitionInterface $definition - * The field storage definition. - * - * @return bool - * Whether the field uses a dedicated table for storage. + * {@inheritdoc} */ - protected function usesDedicatedTable(FieldStorageDefinitionInterface $definition) { - // Everything that is not provided by the entity type is stored in a - // dedicated table. - return $definition->getProvider() != $this->entityType->getProvider() && !$definition->hasCustomStorage(); + public function requiresEntitySchemaChanges(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { + return $this->schemaHandler()->requiresEntitySchemaChanges($definition, $original); } /** * {@inheritdoc} */ - public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { - $schema = $this->_fieldSqlSchema($storage_definition); - foreach ($schema as $name => $table) { - $this->database->schema()->createTable($name, $table); - } + public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { + return $this->schemaHandler()->requiresFieldSchemaChanges($definition, $original); } /** * {@inheritdoc} */ - public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { - if (!$storage_definition->hasData()) { - // There is no data. Re-create the tables completely. + public function requiresEntityDataMigration(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { + return $this->schemaHandler()->requiresEntityDataMigration($definition, $original); + } - if ($this->database->supportsTransactionalDDL()) { - // If the database supports transactional DDL, we can go ahead and rely - // on it. If not, we will have to rollback manually if something fails. - $transaction = $this->database->startTransaction(); - } + /** + * {@inheritdoc} + */ + public function requiresFieldDataMigration(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { + return $this->schemaHandler()->requiresFieldDataMigration($definition, $original); + } - try { - $original_schema = $this->_fieldSqlSchema($original); - foreach ($original_schema as $name => $table) { - $this->database->schema()->dropTable($name, $table); - } - $schema = $this->_fieldSqlSchema($storage_definition); - foreach ($schema as $name => $table) { - $this->database->schema()->createTable($name, $table); - } - } - catch (\Exception $e) { - if ($this->database->supportsTransactionalDDL()) { - $transaction->rollback(); - } - else { - // Recreate tables. - $original_schema = $this->_fieldSqlSchema($original); - foreach ($original_schema as $name => $table) { - if (!$this->database->schema()->tableExists($name)) { - $this->database->schema()->createTable($name, $table); - } - } - } - throw $e; - } - } - else { - if ($storage_definition->getColumns() != $original->getColumns()) { - throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data."); - } - // There is data, so there are no column changes. Drop all the prior - // indexes and create all the new ones, except for all the priors that - // exist unchanged. - $table = static::_fieldTableName($original); - $revision_table = static::_fieldRevisionTableName($original); - - $schema = $storage_definition->getSchema(); - $original_schema = $original->getSchema(); - - foreach ($original_schema['indexes'] as $name => $columns) { - if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) { - $real_name = static::_fieldIndexName($storage_definition, $name); - $this->database->schema()->dropIndex($table, $real_name); - $this->database->schema()->dropIndex($revision_table, $real_name); - } - } - $table = static::_fieldTableName($storage_definition); - $revision_table = static::_fieldRevisionTableName($storage_definition); - foreach ($schema['indexes'] as $name => $columns) { - if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) { - $real_name = static::_fieldIndexName($storage_definition, $name); - $real_columns = array(); - foreach ($columns as $column_name) { - // Indexes can be specified as either a column name or an array with - // column name and length. Allow for either case. - if (is_array($column_name)) { - $real_columns[] = array( - static::_fieldColumnName($storage_definition, $column_name[0]), - $column_name[1], - ); - } - else { - $real_columns[] = static::_fieldColumnName($storage_definition, $column_name); - } - } - $this->database->schema()->addIndex($table, $real_name, $real_columns); - $this->database->schema()->addIndex($revision_table, $real_name, $real_columns); - } - } - } + /** + * {@inheritdoc} + */ + public function onEntityTypeDefinitionCreate() { + $this->schemaHandler()->createEntitySchema($this->entityType); + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeDefinitionDelete() { + $this->schemaHandler()->dropEntitySchema($this->entityType); + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeDefinitionUpdate(EntityTypeInterface $original) { + // Ensure we have an updated entity type definition. + $this->entityType = $this->entityManager->getDefinition($this->entityTypeId); + // The table layout may have changed depending on the new entity type + // definition. + $this->initTableLayout(); + // Let the schema handler adapt to possible table layout changes. + $this->schemaHandler()->updateEntitySchema($this->entityType, $original); + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { + // If we are adding a field stored in a shared table we need to recompute + // the table mapping. + // @todo This does not belong here. Remove it once we are able to generate a + // fresh table mapping in the schema handler. See + // https://www.drupal.org/node/2274017. + if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) { + $this->tableMapping = NULL; + } + $this->schemaHandler()->createFieldSchema($storage_definition); + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + $this->schemaHandler()->updateFieldSchema($storage_definition, $original); } /** * {@inheritdoc} */ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { - // Mark all data associated with the field for deletion. - $table = static::_fieldTableName($storage_definition); - $revision_table = static::_fieldRevisionTableName($storage_definition); - $this->database->update($table) - ->fields(array('deleted' => 1)) - ->execute(); + $table_mapping = $this->getTableMapping(); + + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + // Mark all data associated with the field for deletion. + $table = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition); + $this->database->update($table) + ->fields(array('deleted' => 1)) + ->execute(); + if ($this->entityType->isRevisionable()) { + $this->database->update($revision_table) + ->fields(array('deleted' => 1)) + ->execute(); + } + } - // Move the table to a unique name while the table contents are being - // deleted. - $new_table = static::_fieldTableName($storage_definition, TRUE); - $revision_new_table = static::_fieldRevisionTableName($storage_definition, TRUE); - $this->database->schema()->renameTable($table, $new_table); - $this->database->schema()->renameTable($revision_table, $revision_new_table); + // Update the field schema. + $this->schemaHandler()->prepareFieldSchemaDeletion($storage_definition); } /** * {@inheritdoc} */ public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) { + $table_mapping = $this->getTableMapping(); $storage_definition = $field_definition->getFieldStorageDefinition(); - $table_name = static::_fieldTableName($storage_definition); - $revision_name = static::_fieldRevisionTableName($storage_definition); - // Mark field data as deleted. - $this->database->update($table_name) - ->fields(array('deleted' => 1)) - ->condition('bundle', $field_definition->getBundle()) - ->execute(); - $this->database->update($revision_name) - ->fields(array('deleted' => 1)) - ->condition('bundle', $field_definition->getBundle()) - ->execute(); + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition); + $this->database->update($table_name) + ->fields(array('deleted' => 1)) + ->condition('bundle', $field_definition->getBundle()) + ->execute(); + if ($this->entityType->isRevisionable()) { + $this->database->update($revision_name) + ->fields(array('deleted' => 1)) + ->condition('bundle', $field_definition->getBundle()) + ->execute(); + } + } } /** @@ -1507,21 +1556,24 @@ public function onBundleRename($bundle, $bundle_new) { // @todo Use the unified store of deleted field definitions instead in // https://www.drupal.org/node/2282119 $field_definitions += entity_load_multiple_by_properties('field_instance_config', array('entity_type' => $this->entityTypeId, 'bundle' => $bundle, 'deleted' => TRUE, 'include_deleted' => TRUE)); + $table_mapping = $this->getTableMapping(); foreach ($field_definitions as $field_definition) { $storage_definition = $field_definition->getFieldStorageDefinition(); - if ($this->usesDedicatedTable($storage_definition)) { + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { $is_deleted = $this->storageDefinitionIsDeleted($storage_definition); - $table_name = static::_fieldTableName($storage_definition, $is_deleted); - $revision_name = static::_fieldRevisionTableName($storage_definition, $is_deleted); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted); $this->database->update($table_name) ->fields(array('bundle' => $bundle_new)) ->condition('bundle', $bundle) ->execute(); - $this->database->update($revision_name) - ->fields(array('bundle' => $bundle_new)) - ->condition('bundle', $bundle) - ->execute(); + if ($this->entityType->isRevisionable()) { + $this->database->update($revision_name) + ->fields(array('bundle' => $bundle_new)) + ->condition('bundle', $bundle) + ->execute(); + } } } } @@ -1534,13 +1586,14 @@ protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definit // bundle fields. $storage_definition = $field_definition->getFieldStorageDefinition(); $is_deleted = $this->storageDefinitionIsDeleted($storage_definition); - $table_name = static::_fieldTableName($storage_definition, $is_deleted); + $table_mapping = $this->getTableMapping(); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); // Get the entities which we want to purge first. $entity_query = $this->database->select($table_name, 't', array('fetch' => \PDO::FETCH_ASSOC)); $or = $entity_query->orConditionGroup(); foreach ($storage_definition->getColumns() as $column_name => $data) { - $or->isNotNull(static::_fieldColumnName($storage_definition, $column_name)); + $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name)); } $entity_query ->distinct(TRUE) @@ -1551,7 +1604,7 @@ protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definit // Create a map of field data table column names to field column names. $column_map = array(); foreach ($storage_definition->getColumns() as $column_name => $data) { - $column_map[static::_fieldColumnName($storage_definition, $column_name)] = $column_name; + $column_map[$table_mapping->getFieldColumnName($storage_definition, $column_name)] = $column_name; } $entities = array(); @@ -1591,374 +1644,93 @@ protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definit protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) { $storage_definition = $field_definition->getFieldStorageDefinition(); $is_deleted = $this->storageDefinitionIsDeleted($storage_definition); - $table_name = static::_fieldTableName($storage_definition, $is_deleted); - $revision_name = static::_fieldRevisionTableName($storage_definition, $is_deleted); + $table_mapping = $this->getTableMapping(); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted); $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id(); $this->database->delete($table_name) ->condition('revision_id', $revision_id) ->execute(); - $this->database->delete($revision_name) - ->condition('revision_id', $revision_id) - ->execute(); + if ($this->entityType->isRevisionable()) { + $this->database->delete($revision_name) + ->condition('revision_id', $revision_id) + ->execute(); + } } /** * {@inheritdoc} */ public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) { - $table_name = static::_fieldTableName($storage_definition, TRUE); - $revision_name = static::_fieldRevisionTableName($storage_definition, TRUE); - $this->database->schema()->dropTable($table_name); - $this->database->schema()->dropTable($revision_name); + $this->schemaHandler()->deleteFieldSchema($storage_definition); } /** * {@inheritdoc} */ public function countFieldData($storage_definition, $as_bool = FALSE) { - $is_deleted = $this->storageDefinitionIsDeleted($storage_definition); - $table_name = static::_fieldTableName($storage_definition, $is_deleted); - - $query = $this->database->select($table_name, 't'); - $or = $query->orConditionGroup(); - foreach ($storage_definition->getColumns() as $column_name => $data) { - $or->isNotNull(static::_fieldColumnName($storage_definition, $column_name)); - } - $query - ->condition($or) - ->fields('t', array('entity_id')) - ->distinct(TRUE); - // If we are performing the query just to check if the field has data - // limit the number of rows. - if ($as_bool) { - $query->range(0, 1); - } - $count = $query->countQuery()->execute()->fetchField(); - return $as_bool ? (bool) $count : (int) $count; - } - - /** - * Returns whether the passed field has been already deleted. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field storage definition. - * - * @return bool - * Whether the field has been already deleted. - */ - protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) { - return !array_key_exists($storage_definition->getName(), $this->entityManager->getFieldStorageDefinitions($this->entityTypeId)); - } - - /** - * Gets the SQL table schema. - * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field storage definition. - * @param array $schema - * The field schema array. Mandatory for upgrades, omit otherwise. - * @param bool $deleted - * (optional) Whether the schema of the table holding the values of a - * deleted field should be returned. - * - * @return array - * The same as a hook_schema() implementation for the data and the - * revision tables. - * - * @see hook_schema() - */ - public static function _fieldSqlSchema(FieldStorageDefinitionInterface $storage_definition, array $schema = NULL, $deleted = FALSE) { - $description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; - $description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; - - $entity_type_id = $storage_definition->getTargetEntityTypeId(); - $entity_manager = \Drupal::entityManager(); - $entity_type = $entity_manager->getDefinition($entity_type_id); - $definitions = $entity_manager->getBaseFieldDefinitions($entity_type_id); - - // Define the entity ID schema based on the field definitions. - $id_definition = $definitions[$entity_type->getKey('id')]; - if ($id_definition->getType() == 'integer') { - $id_schema = array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'The entity id this data is attached to', - ); - } - else { - $id_schema = array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'description' => 'The entity id this data is attached to', - ); - } + $table_mapping = $this->getTableMapping(); - // Define the revision ID schema, default to integer if there is no revision - // ID. - $revision_id_definition = $entity_type->hasKey('revision') ? $definitions[$entity_type->getKey('revision')] : NULL; - if (!$revision_id_definition || $revision_id_definition->getType() == 'integer') { - $revision_id_schema = array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - 'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned', - ); + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + $is_deleted = $this->storageDefinitionIsDeleted($storage_definition); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); + $query = $this->database->select($table_name, 't'); + $or = $query->orConditionGroup(); + foreach ($storage_definition->getColumns() as $column_name => $data) { + $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name)); + } + $query + ->condition($or) + ->fields('t', array('entity_id')) + ->distinct(TRUE); + // If we are performing the query just to check if the field has data + // limit the number of rows. + if ($as_bool) { + $query->range(0, 1); + } + $count = $query->countQuery()->execute()->fetchField(); } else { - $revision_id_schema = array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - 'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned', - ); - } - - $current = array( - 'description' => $description_current, - 'fields' => array( - 'bundle' => array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance', - ), - 'deleted' => array( - 'type' => 'int', - 'size' => 'tiny', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'A boolean indicating whether this data item has been deleted' - ), - 'entity_id' => $id_schema, - 'revision_id' => $revision_id_schema, - 'langcode' => array( - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - 'description' => 'The language code for this data item.', - ), - 'delta' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'The sequence number for this data item, used for multi-value fields', - ), - ), - 'primary key' => array('entity_id', 'deleted', 'delta', 'langcode'), - 'indexes' => array( - 'bundle' => array('bundle'), - 'deleted' => array('deleted'), - 'entity_id' => array('entity_id'), - 'revision_id' => array('revision_id'), - 'langcode' => array('langcode'), - ), - ); - - if (!$schema) { - $schema = $storage_definition->getSchema(); - } - - // Add field columns. - foreach ($schema['columns'] as $column_name => $attributes) { - $real_name = static::_fieldColumnName($storage_definition, $column_name); - $current['fields'][$real_name] = $attributes; - } - - // Add unique keys. - foreach ($schema['unique keys'] as $unique_key_name => $columns) { - $real_name = static::_fieldIndexName($storage_definition, $unique_key_name); - foreach ($columns as $column_name) { - $current['unique keys'][$real_name][] = static::_fieldColumnName($storage_definition, $column_name); + if ($as_bool) { + $count = $this->hasData(); } - } - - // Add indexes. - foreach ($schema['indexes'] as $index_name => $columns) { - $real_name = static::_fieldIndexName($storage_definition, $index_name); - foreach ($columns as $column_name) { - // Indexes can be specified as either a column name or an array with - // column name and length. Allow for either case. - if (is_array($column_name)) { - $current['indexes'][$real_name][] = array( - static::_fieldColumnName($storage_definition, $column_name[0]), - $column_name[1], - ); + else { + $data_table = $this->dataTable ?: $this->baseTable; + $query = $this->database->select($data_table, 't'); + $columns = $storage_definition->getColumns(); + if (count($columns) > 1) { + $or = $query->orConditionGroup(); + foreach ($columns as $column_name => $data) { + $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name)); + } + $query->condition($or); } else { - $current['indexes'][$real_name][] = static::_fieldColumnName($storage_definition, $column_name); + $query->isNotNull($storage_definition->getName()); } + $count = $query + ->fields('t', array($this->idKey)) + ->distinct(TRUE) + ->countQuery() + ->execute() + ->fetchField(); } } - // Add foreign keys. - foreach ($schema['foreign keys'] as $specifier => $specification) { - $real_name = static::_fieldIndexName($storage_definition, $specifier); - $current['foreign keys'][$real_name]['table'] = $specification['table']; - foreach ($specification['columns'] as $column_name => $referenced) { - $sql_storage_column = static::_fieldColumnName($storage_definition, $column_name); - $current['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced; - } - } - - // Construct the revision table. - $revision = $current; - $revision['description'] = $description_revision; - $revision['primary key'] = array('entity_id', 'revision_id', 'deleted', 'delta', 'langcode'); - $revision['fields']['revision_id']['not null'] = TRUE; - $revision['fields']['revision_id']['description'] = 'The entity revision id this data is attached to'; - - return array( - static::_fieldTableName($storage_definition) => $current, - static::_fieldRevisionTableName($storage_definition) => $revision, - ); - } - - /** - * Generates a table name for a field data table. - * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. Only - * call this function to write a query that \Drupal::entityQuery() does not - * support. Always call entity_load() before using the data found in the - * table. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field storage definition. - * @param bool $is_deleted - * (optional) Whether the table name holding the values of a deleted field - * should be returned. - * - * @return string - * A string containing the generated name for the database table. - */ - public static function _fieldTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) { - if ($is_deleted) { - // When a field is a deleted, the table is renamed to - // {field_deleted_data_FIELD_UUID}. To make sure we don't end up with - // table names longer than 64 characters, we hash the unique storage - // identifier and return the first 10 characters so we end up with a short - // unique ID. - return "field_deleted_data_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); - } - else { - return static::_generateFieldTableName($storage_definition, FALSE); - } - } - - /** - * Generates a table name for a field revision archive table. - * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. Only - * call this function to write a query that \Drupal::entityQuery() does not - * support. Always call entity_load() before using the data found in the - * table. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field storage definition. - * @param bool $is_deleted - * (optional) Whether the table name holding the values of a deleted field - * should be returned. - * - * @return string - * A string containing the generated name for the database table. - */ - public static function _fieldRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) { - if ($is_deleted) { - // When a field is a deleted, the table is renamed to - // {field_deleted_revision_FIELD_UUID}. To make sure we don't end up with - // table names longer than 64 characters, we hash the unique storage - // identifier and return the first 10 characters so we end up with a short - // unique ID. - return "field_deleted_revision_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); - } - else { - return static::_generateFieldTableName($storage_definition, TRUE); - } - } - - /** - * Generates a safe and unanbiguous field table name. - * - * The method accounts for a maximum table name length of 64 characters, and - * takes care of disambiguation. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field storage definition. - * @param bool $revision - * TRUE for revision table, FALSE otherwise. - * - * @return string - * The final table name. - */ - protected static function _generateFieldTableName(FieldStorageDefinitionInterface $storage_definition, $revision) { - $separator = $revision ? '_revision__' : '__'; - $table_name = $storage_definition->getTargetEntityTypeId() . $separator . $storage_definition->getName(); - // Limit the string to 48 characters, keeping a 16 characters margin for db - // prefixes. - if (strlen($table_name) > 48) { - // Use a shorter separator, a truncated entity_type, and a hash of the - // field UUID. - $separator = $revision ? '_r__' : '__'; - // Truncate to the same length for the current and revision tables. - $entity_type = substr($storage_definition->getTargetEntityTypeId(), 0, 34); - $field_hash = substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); - $table_name = $entity_type . $separator . $field_hash; - } - return $table_name; - } - - /** - * Generates an index name for a field data table. - * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field storage definition. - * @param string $index - * The name of the index. - * - * @return string - * A string containing a generated index name for a field data table that is - * unique among all other fields. - */ - public static function _fieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) { - return $storage_definition->getName() . '_' . $index; + return $as_bool ? (bool) $count : (int) $count; } /** - * Generates a column name for a field data table. - * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. Only - * call this function to write a query that \Drupal::entityQuery() does not - * support. Always call entity_load() before using the data found in the - * table. + * Returns whether the passed field has been already deleted. * * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition * The field storage definition. - * @param string $column - * The name of the column. * - * @return string - * A string containing a generated column name for a field data table that is - * unique among all other fields. + * @return bool + * Whether the field has been already deleted. */ - public static function _fieldColumnName(FieldStorageDefinitionInterface $storage_definition, $column) { - return in_array($column, FieldStorageConfig::getReservedColumns()) ? $column : $storage_definition->getName() . '_' . $column; + protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) { + return !array_key_exists($storage_definition->getName(), $this->entityManager->getFieldStorageDefinitions($this->entityTypeId)); } } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php index 12aeca3..5460436 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php @@ -20,6 +20,20 @@ class ContentEntityNullStorage extends ContentEntityStorageBase { /** * {@inheritdoc} */ + public function hasData() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function countFieldData($storage_definition, $as_bool = FALSE) { + return $as_bool ? FALSE : 0; + } + + /** + * {@inheritdoc} + */ public function loadMultiple(array $ids = NULL) { return array(); } @@ -131,11 +145,4 @@ protected function doSave($id, EntityInterface $entity) { protected function has($id, EntityInterface $entity) { } - /** - * {@inheritdoc} - */ - public function countFieldData($storage_definition, $as_bool = FALSE) { - return $as_bool ? FALSE : 0; - } - } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php index a4849be..a3ca68b 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -78,6 +78,11 @@ protected function doCreate(array $values) { /** * {@inheritdoc} */ + public function onEntityTypeDefinitionUpdate(EntityTypeInterface $original) { } + + /** + * {@inheritdoc} + */ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { } /** diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php index b181bb4..2055eef 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php @@ -97,6 +97,16 @@ public function getEntityType() { /** * {@inheritdoc} */ + public function hasData() { + return (bool) $this->getQuery() + ->range(0, 1) + ->count() + ->execute(); + } + + /** + * {@inheritdoc} + */ public function loadUnchanged($id) { $this->resetCache(array($id)); return $this->load($id); @@ -463,4 +473,22 @@ public function getQuery($conjunction = 'AND') { return \Drupal::entityQuery($this->getEntityTypeId(), $conjunction); } + /** + * {@inheritdoc} + */ + public function onEntityTypeDefinitionCreate() { + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeDefinitionUpdate(EntityTypeInterface $original) { + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeDefinitionDelete() { + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php index 32867f4..9ccabdb 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php @@ -31,6 +31,14 @@ const FIELD_LOAD_REVISION = 'FIELD_LOAD_REVISION'; /** + * Checks whether the storage contains at least one entity. + * + * @return bool + * TRUE if the storage has data, FALSE otherwise. + */ + public function hasData(); + + /** * Resets the internal, static entity cache. * * @param $ids @@ -186,4 +194,22 @@ public function getEntityTypeId(); */ public function getEntityType(); + /** + * Reacts to the creation of the entity type definition. + */ + public function onEntityTypeDefinitionCreate(); + + /** + * Reacts to the update of the entity type definition. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $original + * The original entity type definition. + */ + public function onEntityTypeDefinitionUpdate(EntityTypeInterface $original); + + /** + * Reacts to the deletion of the entity type definition. + */ + public function onEntityTypeDefinitionDelete(); + } diff --git a/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php b/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php index 54e2bff..0149ec3 100644 --- a/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php +++ b/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php @@ -7,7 +7,10 @@ namespace Drupal\Core\Entity\Exception; +use Drupal\Core\Entity\EntityStorageException; + /** * Exception thrown when a storage definition update is forbidden. */ -class FieldStorageDefinitionUpdateForbiddenException extends \Exception { } +class FieldStorageDefinitionUpdateForbiddenException extends EntityStorageException { +} diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php index 23ce020..b1c1a54 100644 --- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php +++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php @@ -10,10 +10,8 @@ use Drupal\Core\Database\Query\SelectInterface; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Entity\Query\QueryException; use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; -use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\FieldStorageConfigInterface; /** @@ -116,11 +114,14 @@ public function addField($field, $type, $langcode) { if ($field_storage instanceof FieldStorageConfigInterface) { // Find the field column. $column = $field_storage->getMainPropertyName(); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping(); + if ($key < $count) { $next = $specifiers[$key + 1]; // Is this a field column? $columns = $field_storage->getColumns(); - if (isset($columns[$next]) || in_array($next, FieldStorageConfig::getReservedColumns())) { + if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) { // Use it. $column = $next; // Do not process it again. @@ -142,7 +143,7 @@ public function addField($field, $type, $langcode) { } } $table = $this->ensureFieldTable($index_prefix, $field_storage, $type, $langcode, $base_table, $entity_id_field, $field_id_field); - $sql_column = ContentEntityDatabaseStorage::_fieldColumnName($field_storage, $column); + $sql_column = $table_mapping->getFieldColumnName($field_storage, $column); } // This is an entity base field (non-configurable field). else { @@ -220,11 +221,13 @@ protected function ensureEntityTable($index_prefix, $property, $type, $langcode, protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field) { $field_name = $field->getName(); if (!isset($this->fieldTables[$index_prefix . $field_name])) { - $table = $this->sqlQuery->getMetaData('age') == EntityStorageInterface::FIELD_LOAD_CURRENT ? ContentEntityDatabaseStorage::_fieldTableName($field) : ContentEntityDatabaseStorage::_fieldRevisionTableName($field); + $entity_type_id = $this->sqlQuery->getMetaData('entity_type'); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping(); + $table = $this->sqlQuery->getMetaData('age') == EntityStorageInterface::FIELD_LOAD_CURRENT ? $table_mapping->getDedicatedDataTableName($field) : $table_mapping->getDedicatedRevisionTableName($field); if ($field->getCardinality() != 1) { $this->sqlQuery->addMetaData('simple_query', FALSE); } - $entity_type = $this->sqlQuery->getMetaData('entity_type'); $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode); } return $this->fieldTables[$index_prefix . $field_name]; diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php index 4f30d47..6863b9b 100644 --- a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php @@ -7,14 +7,27 @@ namespace Drupal\Core\Entity\Schema; +use Drupal\Component\Utility\String; +use Drupal\Core\Database\Connection; use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityStorageException; +use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; +use Drupal\Core\Field\FieldException; +use Drupal\Core\Field\FieldStorageDefinitionInterface; /** * Defines a schema handler that supports revisionable, translatable entities. */ -class ContentEntitySchemaHandler implements EntitySchemaHandlerInterface { +class ContentEntitySchemaHandler implements ContentEntitySchemaHandlerInterface, ContentEntitySchemaProviderInterface { + + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; /** * The entity type this schema builder is responsible for. @@ -31,117 +44,1032 @@ class ContentEntitySchemaHandler implements EntitySchemaHandlerInterface { protected $fieldStorageDefinitions; /** + * The original storage field definitions for this entity type. Used during + * field schema updates. + * + * @var \Drupal\Core\Field\FieldDefinitionInterface[] + */ + protected $originalDefinitions; + + /** * The storage object for the given entity type. * * @var \Drupal\Core\Entity\ContentEntityDatabaseStorage */ - protected $storage; + protected $storage; + + /** + * A static cache of the generated schema array. + * + * @var array + */ + protected $schema; + + /** + * The database connection to be used. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** + * Constructs a ContentEntitySchemaHandler. + * + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type. + * @param \Drupal\Core\Entity\ContentEntityDatabaseStorage $storage + * The storage of the entity type. This must be an SQL-based storage. + * @param \Drupal\Core\Database\Connection $database + * The database connection to be used. + */ + public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorage $storage, Connection $database) { + $this->entityManager = $entity_manager; + $this->entityType = $entity_type; + $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id()); + $this->storage = $storage; + $this->database = $database; + } + + /** + * @return \Drupal\Core\State\StateInterface + */ + protected function state() { + if (!isset($this->state)) { + $this->state = \Drupal::state(); + } + return $this->state; + } + + /** + * {@inheritdoc} + */ + public function requiresEntitySchemaChanges(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { + return !$original || + $original->getStorageClass() != $definition->getStorageClass() || + $original->isRevisionable() != $definition->isRevisionable() || + $original->isTranslatable() != $definition->isTranslatable() || + // Detect changes in key or index definitions. + $this->loadEntitySchemaData($original) != $this->getEntitySchemaData($definition, $this->getEntitySchema($definition, TRUE)); + } + + /** + * {@inheritdoc} + */ + public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { + return !$original || + $original->getSchema() != $definition->getSchema() || + $original->isRevisionable() != $definition->isRevisionable() || + $original->hasCustomStorage() != $definition->hasCustomStorage() || + $this->requiresFieldDataMigration($definition, $original); + } + + /** + * {@inheritdoc} + */ + public function requiresEntityDataMigration(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { + // A change in the storage class may or may not imply a data migration. We + // assume it does. This method should be overridden otherwise. Basically the + // only schema change that does not imply a data migration is from + // revisionable to non revisionable, as in that case we just need to drop + // revision tables. + return $original->getStorageClass() != $definition->getStorageClass() || + $original->isRevisionable() != $definition->isRevisionable() || + $original->isTranslatable() != $definition->isTranslatable(); + } + + /** + * {@inheritdoc} + */ + public function requiresFieldDataMigration(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { + $table_mapping = $this->storage->getTableMapping(); + + // If the field changes its custom storage status, we will need to create or + // drop its schema. In any case we cannot migrate its data as custom storage + // is involved. Otherwise if a field is moved from a shared table to a + // dedicated table or viceversa we need a data migration. + $custom_storage = $original->hasCustomStorage() || $definition->hasCustomStorage(); + $shared_table_changed = $table_mapping->allowsSharedTableStorage($original) != $table_mapping->allowsSharedTableStorage($definition); + $dedicated_table_changed = $table_mapping->requiresDedicatedTableStorage($original) != $table_mapping->requiresDedicatedTableStorage($definition); + if (!$custom_storage && ($shared_table_changed || $dedicated_table_changed)) { + return TRUE; + } + // If columns change we may need data manipulation, which we cannot handle. + if ($original->getColumns() != $definition->getColumns()) { + return TRUE; + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function createEntitySchema(ContentEntityTypeInterface $entity_type) { + $this->checkEntityType($entity_type); + $schema_handler = $this->database->schema(); + $schema = $this->getEntitySchema($entity_type, TRUE); + foreach ($schema as $table_name => $table_schema) { + if (!$schema_handler->tableExists($table_name)) { + $schema_handler->createTable($table_name, $table_schema); + } + } + $this->saveEntitySchemaData($entity_type, $schema); + } + + /** + * {@inheritdoc} + */ + public function dropEntitySchema(ContentEntityTypeInterface $entity_type) { + $this->checkEntityType($entity_type); + $schema_handler = $this->database->schema(); + $actual_definition = $this->entityManager->getDefinition($entity_type->id()); + // @todo Instead of switching the wrapped entity type, we should be able to + // instantiate a new table mapping for each entity type definition. See + // https://www.drupal.org/node/2274017. + $this->storage->setEntityType($entity_type); + + foreach ($this->getEntitySchemaTables() as $table_name) { + if ($schema_handler->tableExists($table_name)) { + $schema_handler->dropTable($table_name); + } + } + + $this->storage->setEntityType($actual_definition); + } + + /** + * {@inheritdoc} + */ + public function updateEntitySchema(ContentEntityTypeInterface $entity_type, ContentEntityTypeInterface $original) { + $this->checkEntityType($entity_type); + $this->checkEntityType($original); + + // If we have no data just recreate the entity schema from scratch. + if (!$this->database->schema()->tableExists($this->storage->getBaseTable()) || !$this->storage->hasData()) { + if ($this->database->supportsTransactionalDDL()) { + // If the database supports transactional DDL, we can go ahead and rely + // on it. If not, we will have to rollback manually if something fails. + $transaction = $this->database->startTransaction(); + } + try { + $this->dropEntitySchema($original); + $this->createEntitySchema($entity_type); + + // Update dedicated table revision schema. + if ($original->isRevisionable() && !$entity_type->isRevisionable()) { + $this->dropDedicatedTableRevisionSchema(); + } + elseif (!$original->isRevisionable() && $entity_type->isRevisionable()) { + $this->createDedicatedTableRevisionSchema($entity_type); + } + } + catch (\Exception $e) { + if ($this->database->supportsTransactionalDDL()) { + $transaction->rollback(); + } + else { + // Recreate original schema. + $this->createEntitySchema($original); + } + throw $e; + } + } + else { + $schema_handler = $this->database->schema(); + + // Drop original indexes and unique keys. + foreach ($this->loadEntitySchemaData($entity_type) as $table_name => $schema) { + if (!empty($schema['indexes'])) { + foreach ($schema['indexes'] as $name => $specifier) { + $schema_handler->dropIndex($table_name, $name); + } + } + if (!empty($schema['unique keys'])) { + foreach ($schema['unique keys'] as $name => $specifier) { + $schema_handler->dropUniqueKey($table_name, $name); + } + } + } + + // Create new indexes and unique keys. + $entity_schema = $this->getEntitySchema($entity_type, TRUE); + foreach ($this->getEntitySchemaData($entity_type, $entity_schema) as $table_name => $schema) { + if (!empty($schema['indexes'])) { + foreach ($schema['indexes'] as $name => $specifier) { + $schema_handler->addIndex($table_name, $name, $specifier); + } + } + if (!empty($schema['unique keys'])) { + foreach ($schema['unique keys'] as $name => $specifier) { + $schema_handler->addUniqueKey($table_name, $name, $specifier); + } + } + } + + // Store the updated entity schema. + $this->saveEntitySchemaData($entity_type, $entity_schema); + } + } + + /** + * Creates revision tables for the specified entity type. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type definition. + */ + protected function createDedicatedTableRevisionSchema(ContentEntityTypeInterface $entity_type) { + $table_mapping = $this->storage->getTableMapping(); + $schema_manager = $this->database->schema(); + foreach ($this->fieldStorageDefinitions as $definition) { + if ($table_mapping->requiresDedicatedTableStorage($definition)) { + $schema = $this->getDedicatedTableSchema($definition, $entity_type); + $table_name = $table_mapping->getDedicatedRevisionTableName($definition); + $schema_manager->createTable($table_name, $schema[$table_name]); + } + } + } + + /** + * Deletes revision tables for the specified entity type. + */ + protected function dropDedicatedTableRevisionSchema() { + $table_mapping = $this->storage->getTableMapping(); + $schema_manager = $this->database->schema(); + foreach ($this->fieldStorageDefinitions as $definition) { + if ($table_mapping->requiresDedicatedTableStorage($definition)) { + $schema_manager->dropTable($table_mapping->getDedicatedRevisionTableName($definition)); + } + } + } + + /** + * Returns the entity schema for the specified entity type. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type definition. + * @param bool $reset + * (optional) If set to TRUE static cache will be ignored and a new schema + * array generation will be performed. Defaults to FALSE. + * + * @return array + * A Schema API array describing the entity schema, excluding dedicated + * field tables. + * + * @throws \Drupal\Core\Field\FieldException + */ + protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) { + $this->checkEntityType($entity_type); + $entity_type_id = $entity_type->id(); + + if (!isset($this->schema[$entity_type_id]) || $reset) { + // Back up the storage definition and replace it with the passed one. + // @todo Instead of switching the wrapped entity type, we should be able + // to instantiate a new table mapping for each entity type definition. + // See https://www.drupal.org/node/2274017. + $actual_definition = $this->entityManager->getDefinition($entity_type_id); + $this->storage->setEntityType($entity_type); + + // Prepare basic information about the entity type. + $tables = $this->getEntitySchemaTables(); + + // Initialize the table schema. + $schema[$tables['base_table']] = $this->initializeBaseTable($entity_type); + if (isset($tables['revision_table'])) { + $schema[$tables['revision_table']] = $this->initializeRevisionTable($entity_type); + } + if (isset($tables['data_table'])) { + $schema[$tables['data_table']] = $this->initializeDataTable($entity_type); + } + if (isset($tables['revision_data_table'])) { + $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable($entity_type); + } + + // We need to act only on shared entity schema tables. + $table_mapping = $this->storage->getTableMapping(); + $table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()); + $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); + foreach ($table_names as $table_name) { + if (!isset($schema[$table_name])) { + $schema[$table_name] = array(); + } + foreach ($table_mapping->getFieldNames($table_name) as $field_name) { + if (!isset($storage_definitions[$field_name])) { + throw new FieldException(String::format('Fieled storage definition for "@field_name" could not be found.', array('@field_name' => $field_name))); + } + // Add the schema for base field definitions. + elseif ($table_mapping->allowsSharedTableStorage($storage_definitions[$field_name])) { + $column_names = $table_mapping->getColumnNames($field_name); + $storage_definition = $storage_definitions[$field_name]; + $schema[$table_name] = array_merge_recursive($schema[$table_name], $this->getSharedTableFieldSchema($storage_definition, $column_names)); + } + } + + // Add the schema for extra fields. + foreach ($table_mapping->getExtraColumns($table_name) as $column_name) { + if ($column_name == 'default_langcode') { + $this->addDefaultLangcodeSchema($schema[$table_name]); + } + } + } + + // Process tables after having gathered field information. + $this->processBaseTable($entity_type, $schema[$tables['base_table']]); + if (isset($tables['revision_table'])) { + $this->processRevisionTable($entity_type, $schema[$tables['revision_table']]); + } + if (isset($tables['data_table'])) { + $this->processDataTable($entity_type, $schema[$tables['data_table']]); + } + if (isset($tables['revision_data_table'])) { + $this->processRevisionDataTable($entity_type, $schema[$tables['revision_data_table']]); + } + + $this->schema[$entity_type_id] = $schema; + + // Restore the actual definition. + $this->storage->setEntityType($actual_definition); + } + + return $this->schema[$entity_type_id]; + } + + /** + * Checks that we are dealing with the correct entity type. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type to be checked. + * + * @return bool + * TRUE if the entity type matches the current one. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + protected function checkEntityType(ContentEntityTypeInterface $entity_type) { + if ($entity_type->id() != $this->entityType->id()) { + throw new EntityStorageException(String::format('Unsupported entity type @id', array('@id' => $entity_type->id()))); + } + return TRUE; + } + + /** + * Gets a list of entity type tables. + * + * @return array + * A list of entity type tables, keyed by table key. + */ + protected function getEntitySchemaTables() { + return array_filter(array( + 'base_table' => $this->storage->getBaseTable(), + 'revision_table' => $this->storage->getRevisionTable(), + 'data_table' => $this->storage->getDataTable(), + 'revision_data_table' => $this->storage->getRevisionDataTable(), + )); + } + + /** + * Returns entity schema definitions for index and key definitions. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type definition. + * @param array $schema + * The entity schema array. + * + * @return array + * A stripped down version of the $schema Schema API array containing, for + * each table, only the key and index definitions not derived from field + * storage definitions. + */ + protected function getEntitySchemaData(ContentEntityTypeInterface $entity_type, array $schema) { + $schema_data = array(); + $entity_type_id = $entity_type->id(); + $keys = array('indexes', 'unique keys'); + $unused_keys = array_flip(array('description', 'fields', 'foreign keys')); + + foreach ($schema as $table_name => $table_schema) { + $table_schema = array_diff_key($table_schema, $unused_keys); + foreach ($keys as $key) { + // Exclude data generated from field storage definitions, we will check + // that separately. + if (!empty($table_schema[$key])) { + $data_keys = array_keys($table_schema[$key]); + $entity_keys = array_filter($data_keys, function ($key) use ($entity_type_id) { + return strpos($key, $entity_type_id . '_field_') !== 0; + }); + $table_schema[$key] = array_intersect_key($table_schema[$key], array_flip($entity_keys)); + } + } + $schema_data[$table_name] = array_filter($table_schema); + } + + return $schema_data; + } + + /** + * Loads stored schema data for the given entity type definition. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type definition. + * + * @return array + * The entity schema data array. + */ + protected function loadEntitySchemaData(ContentEntityTypeInterface $entity_type) { + return $this->state()->get('entity.schema.handler.' . $entity_type->id() . '.schema_data') ?: array(); + } + + /** + * Stores schema data for the given entity type definition. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type definition. + * @param array $schema + * The entity schema data array. + */ + protected function saveEntitySchemaData(ContentEntityTypeInterface $entity_type, $schema) { + $data = $this->getEntitySchemaData($entity_type, $schema); + $this->state()->set('entity.schema.handler.' . $entity_type->id() . '.schema_data', $data); + } + + /** + * Initializes common information for a base table. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type. + * + * @return array + * A partial schema array for the base table. + */ + protected function initializeBaseTable(ContentEntityTypeInterface $entity_type) { + $entity_type_id = $entity_type->id(); + + $schema = array( + 'description' => "The base table for $entity_type_id entities.", + 'primary key' => array($entity_type->getKey('id')), + 'indexes' => array(), + 'foreign keys' => array(), + ); + + if ($entity_type->hasKey('revision')) { + $revision_key = $entity_type->getKey('revision'); + $key_name = $this->getEntityIndexName($entity_type, $revision_key); + $schema['unique keys'][$key_name] = array($revision_key); + $schema['foreign keys'][$entity_type_id . '__revision'] = array( + 'table' => $this->storage->getRevisionTable(), + 'columns' => array($revision_key => $revision_key), + ); + } + + $this->addTableDefaults($schema); + + return $schema; + } + + /** + * Initializes common information for a revision table. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type. + * + * @return array + * A partial schema array for the revision table. + */ + protected function initializeRevisionTable(ContentEntityTypeInterface $entity_type) { + $entity_type_id = $entity_type->id(); + $id_key = $entity_type->getKey('id'); + $revision_key = $entity_type->getKey('revision'); + + $schema = array( + 'description' => "The revision table for $entity_type_id entities.", + 'primary key' => array($revision_key), + 'indexes' => array(), + 'foreign keys' => array( + $entity_type_id . '__revisioned' => array( + 'table' => $this->storage->getBaseTable(), + 'columns' => array($id_key => $id_key), + ), + ), + ); + + $schema['indexes'][$this->getEntityIndexName($entity_type, $id_key)] = array($id_key); + + $this->addTableDefaults($schema); + + return $schema; + } + + /** + * Initializes common information for a data table. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type. + * + * @return array + * A partial schema array for the data table. + */ + protected function initializeDataTable(ContentEntityTypeInterface $entity_type) { + $entity_type_id = $entity_type->id(); + $id_key = $entity_type->getKey('id'); + + $schema = array( + 'description' => "The data table for $entity_type_id entities.", + // @todo Use the language entity key when https://drupal.org/node/2143729 + // is in. + 'primary key' => array($id_key, 'langcode'), + 'indexes' => array(), + 'foreign keys' => array( + $entity_type_id => array( + 'table' => $this->storage->getBaseTable(), + 'columns' => array($id_key => $id_key), + ), + ), + ); + + if ($entity_type->hasKey('revision')) { + $key = $entity_type->getKey('revision'); + $schema['indexes'][$this->getEntityIndexName($entity_type, $key)] = array($key); + } + + $this->addTableDefaults($schema); + + return $schema; + } + + /** + * Initializes common information for a revision data table. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type. + * + * @return array + * A partial schema array for the revision data table. + */ + protected function initializeRevisionDataTable(ContentEntityTypeInterface $entity_type) { + $entity_type_id = $entity_type->id(); + $id_key = $entity_type->getKey('id'); + $revision_key = $entity_type->getKey('revision'); + + $schema = array( + 'description' => "The revision data table for $entity_type_id entities.", + // @todo Use the language entity key when https://drupal.org/node/2143729 + // is in. + 'primary key' => array($revision_key, 'langcode'), + 'indexes' => array(), + 'foreign keys' => array( + $entity_type_id => array( + 'table' => $this->storage->getBaseTable(), + 'columns' => array($id_key => $id_key), + ), + $entity_type_id . '__revision' => array( + 'table' => $this->storage->getRevisionTable(), + 'columns' => array($revision_key => $revision_key), + ) + ), + ); + + $this->addTableDefaults($schema); + + return $schema; + } + + /** + * Processes the gathered schema for a base table. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type. + * @param array $schema + * The table schema, passed by reference. + * + * @return array + * A partial schema array for the base table. + */ + protected function processBaseTable(ContentEntityTypeInterface $entity_type, array &$schema) { + $this->processIdentifierSchema($schema, $entity_type->getKey('id')); + } + + /** + * Processes the gathered schema for a base table. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type. + * @param array $schema + * The table schema, passed by reference. + * + * @return array + * A partial schema array for the base table. + */ + protected function processRevisionTable(ContentEntityTypeInterface $entity_type, array &$schema) { + $this->processIdentifierSchema($schema, $entity_type->getKey('revision')); + } + + /** + * Processes the gathered schema for a base table. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type. + * @param array $schema + * The table schema, passed by reference. + * + * @return array + * A partial schema array for the base table. + */ + protected function processDataTable(ContentEntityTypeInterface $entity_type, array &$schema) { + } + + /** + * Processes the gathered schema for a base table. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type. + * @param array $schema + * The table schema, passed by reference. + * + * @return array + * A partial schema array for the base table. + */ + protected function processRevisionDataTable(ContentEntityTypeInterface $entity_type, array &$schema) { + } + + /** + * Performs the specified operation on a field. + * + * This figures out whether the field is stored in a dedicated or shared table + * and forwards the call to the proper handler. + * + * @param string $operation + * The name of the operation to be performed. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original + * (optional) The original field storage definition. This is relevant (and + * required) only for updates. Defaults to NULL. + */ + protected function performFieldSchemaOperation($operation, FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original = NULL) { + $table_mapping = $this->storage->getTableMapping(); + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + $this->{$operation . 'DedicatedTableSchema'}($storage_definition, $original); + } + elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) { + $this->{$operation . 'SharedTableSchema'}($storage_definition, $original); + } + } + + /** + * {@inheritdoc} + */ + public function createFieldSchema(FieldStorageDefinitionInterface $storage_definition) { + $this->performFieldSchemaOperation('create', $storage_definition); + } + + /** + * Creates the schema for a field stored in a dedicated table. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The storage definition of the field being created. + */ + protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) { + $schema = $this->getDedicatedTableSchema($storage_definition); + foreach ($schema as $name => $table) { + $this->database->schema()->createTable($name, $table); + } + } + + /** + * Creates the schema for a field stored in a shared table. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The storage definition of the field being created. + */ + protected function createSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) { + $created_field_name = $storage_definition->getName(); + $table_mapping = $this->storage->getTableMapping(); + $column_names = $table_mapping->getColumnNames($created_field_name); + $schema = $this->getSharedTableFieldSchema($storage_definition, $column_names); + $keys = array_diff_key($schema, array('fields' => FALSE)); + $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()); + + // Iterate over the mapped table to find the ones that will host the created + // field schema. + foreach ($shared_table_names as $table_name) { + foreach ($table_mapping->getFieldNames($table_name) as $field_name) { + if ($field_name == $created_field_name) { + foreach ($schema['fields'] as $column_name => $specifier) { + $this->database->schema()->addField($table_name, $column_name, $specifier, $keys); + } + // After creating the field schema skip to the next table. + break; + } + } + } + } + + /** + * {@inheritdoc} + */ + public function prepareFieldSchemaDeletion(FieldStorageDefinitionInterface $storage_definition) { + $table_mapping = $this->storage->getTableMapping(); + // @todo Implement this also for shared table storage. See + // https://www.drupal.org/node/2282119. + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + // Move the table to a unique name while the table contents are being + // deleted. + $table = $table_mapping->getDedicatedDataTableName($storage_definition); + $new_table = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE); + $this->database->schema()->renameTable($table, $new_table); + if ($this->entityType->isRevisionable()) { + $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition); + $revision_new_table = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE); + $this->database->schema()->renameTable($revision_table, $revision_new_table); + } + } + } + + /** + * {@inheritdoc} + */ + public function deleteFieldSchema(FieldStorageDefinitionInterface $storage_definition) { + $this->performFieldSchemaOperation('delete', $storage_definition); + } /** - * A static cache of the generated schema array. + * Deletes the schema for a field stored in a dedicated table. * - * @var array + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The storage definition of the field being deleted. */ - protected $schema; + protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) { + // When switching from dedicated to shared field table layout we need need + // to delete the field tables with their regular names. When this happens + // original definitions will be defined. + $deleted = !$this->originalDefinitions; + $table_mapping = $this->storage->getTableMapping(); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $deleted); + $this->database->schema()->dropTable($table_name); + if ($this->entityType->isRevisionable()) { + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $deleted); + $this->database->schema()->dropTable($revision_name); + } + } /** - * Constructs a ContentEntitySchemaHandler. + * Deletes the schema for a field stored in a shared table. * - * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager - * The entity manager. - * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type - * The entity type. - * @param \Drupal\Core\Entity\ContentEntityDatabaseStorage $storage - * The storage of the entity type. This must be an SQL-based storage. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The storage definition of the field being deleted. */ - public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorage $storage) { - $this->entityType = $entity_type; - $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id()); - $this->storage = $storage; + protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) { + $deleted_field_name = $storage_definition->getName(); + $table_mapping = $this->storage->getTableMapping($this->originalDefinitions); + $column_names = $table_mapping->getColumnNames($deleted_field_name); + $schema = $this->getSharedTableFieldSchema($storage_definition, $column_names); + $schema_handler = $this->database->schema(); + $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()); + + // Iterate over the mapped table to find the ones that host the deleted + // field schema. + foreach ($shared_table_names as $table_name) { + foreach ($table_mapping->getFieldNames($table_name) as $field_name) { + if ($field_name == $deleted_field_name) { + // Drop indexes and unique keys first. + if (!empty($schema['indexes'])) { + foreach ($schema['indexes'] as $name => $specifier) { + $schema_handler->dropIndex($table_name, $name); + } + } + if (!empty($schema['unique keys'])) { + foreach ($schema['unique keys'] as $name => $specifier) { + $schema_handler->dropUniqueKey($table_name, $name); + } + } + // Drop columns. + foreach ($column_names as $column_name) { + $schema_handler->dropField($table_name, $column_name); + } + // After deleting the field schema skip to the next table. + break; + } + } + } } /** * {@inheritdoc} */ - public function getSchema() { - // Prepare basic information about the entity type. - $tables = $this->getTables(); + public function updateFieldSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + // Store original definitions so that switching between shared and dedicated + // field table layout works. + $this->originalDefinitions = $this->fieldStorageDefinitions; + $this->originalDefinitions[$original->getName()] = $original; + $this->performFieldSchemaOperation('update', $storage_definition, $original); + $this->originalDefinitions = NULL; + } - if (!isset($this->schema[$this->entityType->id()])) { - // Initialize the table schema. - $schema[$tables['base_table']] = $this->initializeBaseTable(); - if (isset($tables['revision_table'])) { - $schema[$tables['revision_table']] = $this->initializeRevisionTable(); + /** + * Updates the schema for a field stored in a shared table. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The storage definition of the field being updated. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original + * The original storage definition; i.e., the definition before the update. + * + * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException + * Thrown when the update to the field is forbidden. + * @throws \Exception + * Rethrown exception if the table recreation fails. + */ + protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + if (!$this->storage->countFieldData($original, TRUE)) { + // There is no data. Re-create the tables completely. + if ($this->database->supportsTransactionalDDL()) { + // If the database supports transactional DDL, we can go ahead and rely + // on it. If not, we will have to rollback manually if something fails. + $transaction = $this->database->startTransaction(); } - if (isset($tables['data_table'])) { - $schema[$tables['data_table']] = $this->initializeDataTable(); + try { + // Since there is no data we may be switching from a shared table schema + // to a dedicated table schema, hence we should use the proper API. + $this->deleteFieldSchema($original); + $this->createFieldSchema($storage_definition); } - if (isset($tables['revision_data_table'])) { - $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable(); + catch (\Exception $e) { + if ($this->database->supportsTransactionalDDL()) { + $transaction->rollback(); + } + else { + // Recreate tables. + $this->createFieldSchema($original); + } + throw $e; } - + } + else { + if ($storage_definition->getColumns() != $original->getColumns()) { + throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data."); + } + // There is data, so there are no column changes. Drop all the prior + // indexes and create all the new ones, except for all the priors that + // exist unchanged. $table_mapping = $this->storage->getTableMapping(); - foreach ($table_mapping->getTableNames() as $table_name) { - // Add the schema from field definitions. - foreach ($table_mapping->getFieldNames($table_name) as $field_name) { - $column_names = $table_mapping->getColumnNames($field_name); - $this->addFieldSchema($schema[$table_name], $field_name, $column_names); - } + $table = $table_mapping->getDedicatedDataTableName($original); + $revision_table = $table_mapping->getDedicatedRevisionTableName($original); - // Add the schema for extra fields. - foreach ($table_mapping->getExtraColumns($table_name) as $column_name) { - if ($column_name == 'default_langcode') { - $this->addDefaultLangcodeSchema($schema[$table_name]); + $schema = $storage_definition->getSchema(); + $original_schema = $original->getSchema(); + + foreach ($original_schema['indexes'] as $name => $columns) { + if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) { + $real_name = $this->getFieldIndexName($storage_definition, $name); + $this->database->schema()->dropIndex($table, $real_name); + $this->database->schema()->dropIndex($revision_table, $real_name); + } + } + $table = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition); + foreach ($schema['indexes'] as $name => $columns) { + if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) { + $real_name = $this->getFieldIndexName($storage_definition, $name); + $real_columns = array(); + foreach ($columns as $column_name) { + // Indexes can be specified as either a column name or an array with + // column name and length. Allow for either case. + if (is_array($column_name)) { + $real_columns[] = array( + $table_mapping->getFieldColumnName($storage_definition, $column_name[0]), + $column_name[1], + ); + } + else { + $real_columns[] = $table_mapping->getFieldColumnName($storage_definition, $column_name); + } } + $this->database->schema()->addIndex($table, $real_name, $real_columns); + $this->database->schema()->addIndex($revision_table, $real_name, $real_columns); } } + } + } - // Process tables after having gathered field information. - $this->processBaseTable($schema[$tables['base_table']]); - if (isset($tables['revision_table'])) { - $this->processRevisionTable($schema[$tables['revision_table']]); + /** + * Updates the schema for a field stored in a shared table. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The storage definition of the field being updated. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original + * The original storage definition; i.e., the definition before the update. + * + * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException + * Thrown when the update to the field is forbidden. + * @throws \Exception + * Rethrown exception if the table recreation fails. + */ + protected function updateSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + if (!$this->storage->countFieldData($original, TRUE)) { + if ($this->database->supportsTransactionalDDL()) { + // If the database supports transactional DDL, we can go ahead and rely + // on it. If not, we will have to rollback manually if something fails. + $transaction = $this->database->startTransaction(); } - if (isset($tables['data_table'])) { - $this->processDataTable($schema[$tables['data_table']]); + try { + // Since there is no data we may be switching from a dedicated table + // to a schema table schema, hence we should use the proper API. + $this->deleteFieldSchema($original); + $this->createFieldSchema($storage_definition); } - if (isset($tables['revision_data_table'])) { - $this->processRevisionDataTable($schema[$tables['revision_data_table']]); + catch (\Exception $e) { + if ($this->database->supportsTransactionalDDL()) { + $transaction->rollback(); + } + else { + // Recreate original schema. + $this->createSharedTableSchema($original); + } + throw $e; } - - $this->schema[$this->entityType->id()] = $schema; } + else { + if ($storage_definition->getColumns() != $original->getColumns()) { + throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data."); + } - return $this->schema[$this->entityType->id()]; - } + $updated_field_name = $storage_definition->getName(); + $table_mapping = $this->storage->getTableMapping(); + $column_names = $table_mapping->getColumnNames($updated_field_name); + $original_schema = $this->getSharedTableFieldSchema($original, $column_names); + $schema = $this->getSharedTableFieldSchema($storage_definition, $column_names); + $schema_handler = $this->database->schema(); - /** - * Gets a list of entity type tables. - * - * @return array - * A list of entity type tables, keyed by table key. - */ - protected function getTables() { - return array_filter(array( - 'base_table' => $this->storage->getBaseTable(), - 'revision_table' => $this->storage->getRevisionTable(), - 'data_table' => $this->storage->getDataTable(), - 'revision_data_table' => $this->storage->getRevisionDataTable(), - )); + // Iterate over the mapped table to find the ones that host the deleted + // field schema. + foreach ($table_mapping->getTableNames() as $table_name) { + foreach ($table_mapping->getFieldNames($table_name) as $field_name) { + if ($field_name == $updated_field_name) { + // Drop original indexes and unique keys. + if (!empty($original_schema['indexes'])) { + foreach ($original_schema['indexes'] as $name => $specifier) { + $schema_handler->dropIndex($table_name, $name); + } + } + if (!empty($original_schema['unique keys'])) { + foreach ($original_schema['unique keys'] as $name => $specifier) { + $schema_handler->dropUniqueKey($table_name, $name); + } + } + // Create new indexes and unique keys. + if (!empty($schema['indexes'])) { + foreach ($schema['indexes'] as $name => $specifier) { + $schema_handler->addIndex($table_name, $name, $specifier); + } + } + if (!empty($schema['unique keys'])) { + foreach ($schema['unique keys'] as $name => $specifier) { + $schema_handler->addUniqueKey($table_name, $name, $specifier); + } + } + // After deleting the field schema skip to the next table. + break; + } + } + } + } } /** * Returns the schema for a single field definition. * - * @param array $schema - * The table schema to add the field schema to, passed by reference. - * @param string $field_name - * The name of the field. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The storage definition of the field whose schema has to be returned. * @param string[] $column_mapping * A mapping of field column names to database column names. + * + * @return array + * The schema definition for the table with the following keys: + * - fields: The schema definition for the each field columns. + * - indexes: The schema definition for the indexes. + * - unique keys: The schema definition for the unique keys. + * - foreign keys: The schema definition for the foreign keys. + * + * @throws \Drupal\Core\Field\FieldException + * Exception thrown if the schema contains reserved column names. */ - protected function addFieldSchema(array &$schema, $field_name, array $column_mapping) { - $field_schema = $this->fieldStorageDefinitions[$field_name]->getSchema(); - $field_description = $this->fieldStorageDefinitions[$field_name]->getDescription(); + protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, array $column_mapping) { + $schema = array(); + $field_schema = $storage_definition->getSchema(); + + // Check that the schema does not include forbidden column names. + if (array_intersect(array_keys($field_schema['columns']), $this->storage->getTableMapping()->getReservedColumns())) { + throw new FieldException(format_string('Illegal field column names on @field_name', array('@field_name' => $storage_definition->getName()))); + } + + $field_name = $storage_definition->getName(); + $field_description = $storage_definition->getDescription(); foreach ($column_mapping as $field_column_name => $schema_field_name) { $column_schema = $field_schema['columns'][$field_column_name]; @@ -166,19 +1094,18 @@ protected function addFieldSchema(array &$schema, $field_name, array $column_map } if (!empty($field_schema['indexes'])) { - $indexes = $this->getFieldIndexes($field_name, $field_schema, $column_mapping); - $schema['indexes'] = array_merge($schema['indexes'], $indexes); + $schema['indexes'] = $this->getFieldIndexes($field_name, $field_schema, $column_mapping); } if (!empty($field_schema['unique keys'])) { - $unique_keys = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping); - $schema['unique keys'] = array_merge($schema['unique keys'], $unique_keys); + $schema['unique keys'] = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping); } if (!empty($field_schema['foreign keys'])) { - $foreign_keys = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping); - $schema['foreign keys'] = array_merge($schema['foreign keys'], $foreign_keys); + $schema['foreign keys'] = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping); } + + return $schema; } /** @@ -337,133 +1264,170 @@ protected function addDefaultLangcodeSchema(&$schema) { ); } + /** - * Initializes common information for a base table. + * Returns the SQL schema for a dedicated table. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * (optional) The entity type definition. Defaults to the one returned by + * the entity manager. * * @return array - * A partial schema array for the base table. + * The schema definition for the table with the following keys: + * - fields: The schema definition for the each field columns. + * - indexes: The schema definition for the indexes. + * - unique keys: The schema definition for the unique keys. + * - foreign keys: The schema definition for the foreign keys. + * + * @throws \Drupal\Core\Field\FieldException + * Exception thrown if the schema contains reserved column names. + * + * @see hook_schema() */ - protected function initializeBaseTable() { - $entity_type_id = $this->entityType->id(); + protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, ContentEntityTypeInterface $entity_type = NULL) { + $description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; + $description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; - $schema = array( - 'description' => "The base table for $entity_type_id entities.", - 'primary key' => array($this->entityType->getKey('id')), - 'indexes' => array(), - 'foreign keys' => array(), - ); - - if ($this->entityType->hasKey('revision')) { - $revision_key = $this->entityType->getKey('revision'); - $key_name = $this->getEntityIndexName($revision_key); - $schema['unique keys'][$key_name] = array($revision_key); - $schema['foreign keys'][$entity_type_id . '__revision'] = array( - 'table' => $this->storage->getRevisionTable(), - 'columns' => array($revision_key => $revision_key), + $id_definition = $this->fieldStorageDefinitions[$this->entityType->getKey('id')]; + if ($id_definition->getType() == 'integer') { + $id_schema = array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The entity id this data is attached to', + ); + } + else { + $id_schema = array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'description' => 'The entity id this data is attached to', ); } - $this->addTableDefaults($schema); - - return $schema; - } - - /** - * Initializes common information for a revision table. - * - * @return array - * A partial schema array for the revision table. - */ - protected function initializeRevisionTable() { - $entity_type_id = $this->entityType->id(); - $id_key = $this->entityType->getKey('id'); - $revision_key = $this->entityType->getKey('revision'); + // Define the revision ID schema, default to integer if there is no revision + // ID. + // @todo Revisit this code: the revision id should match the entity id type + // if revisions are not supported. + $revision_id_definition = $this->entityType->isRevisionable() ? $this->fieldStorageDefinitions[$this->entityType->getKey('revision')] : NULL; + if (!$revision_id_definition || $revision_id_definition->getType() == 'integer') { + $revision_id_schema = array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned', + ); + } + else { + $revision_id_schema = array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => FALSE, + 'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned', + ); + } - $schema = array( - 'description' => "The revision table for $entity_type_id entities.", - 'primary key' => array($revision_key), - 'indexes' => array(), - 'foreign keys' => array( - $entity_type_id . '__revisioned' => array( - 'table' => $this->storage->getBaseTable(), - 'columns' => array($id_key => $id_key), + $data_schema = array( + 'description' => $description_current, + 'fields' => array( + 'bundle' => array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance', ), - ), - ); - - $schema['indexes'][$this->getEntityIndexName($id_key)] = array($id_key); - - $this->addTableDefaults($schema); - - return $schema; - } - - /** - * Initializes common information for a data table. - * - * @return array - * A partial schema array for the data table. - */ - protected function initializeDataTable() { - $entity_type_id = $this->entityType->id(); - $id_key = $this->entityType->getKey('id'); - - $schema = array( - 'description' => "The data table for $entity_type_id entities.", - // @todo Use the language entity key when https://drupal.org/node/2143729 - // is in. - 'primary key' => array($id_key, 'langcode'), - 'indexes' => array(), - 'foreign keys' => array( - $entity_type_id => array( - 'table' => $this->storage->getBaseTable(), - 'columns' => array($id_key => $id_key), + 'deleted' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'A boolean indicating whether this data item has been deleted' + ), + 'entity_id' => $id_schema, + 'revision_id' => $revision_id_schema, + 'langcode' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The language code for this data item.', + ), + 'delta' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The sequence number for this data item, used for multi-value fields', ), ), + 'primary key' => array('entity_id', 'deleted', 'delta', 'langcode'), + 'indexes' => array( + 'bundle' => array('bundle'), + 'deleted' => array('deleted'), + 'entity_id' => array('entity_id'), + 'revision_id' => array('revision_id'), + 'langcode' => array('langcode'), + ), ); - if ($this->entityType->hasKey('revision')) { - $key = $this->entityType->getKey('revision'); - $schema['indexes'][$this->getEntityIndexName($key)] = array($key); + // Check that the schema does not include forbidden column names. + $schema = $storage_definition->getSchema(); + $table_mapping = $this->storage->getTableMapping(); + if (array_intersect(array_keys($schema['columns']), $table_mapping->getReservedColumns())) { + throw new FieldException(format_string('Illegal field column names on @field_name', array('@field_name' => $storage_definition->getName()))); } - $this->addTableDefaults($schema); + // Add field columns. + foreach ($schema['columns'] as $column_name => $attributes) { + $real_name = $table_mapping->getFieldColumnName($storage_definition, $column_name); + $data_schema['fields'][$real_name] = $attributes; + } - return $schema; - } + // Add indexes. + foreach ($schema['indexes'] as $index_name => $columns) { + $real_name = $this->getFieldIndexName($storage_definition, $index_name); + foreach ($columns as $column_name) { + // Indexes can be specified as either a column name or an array with + // column name and length. Allow for either case. + if (is_array($column_name)) { + $data_schema['indexes'][$real_name][] = array( + $table_mapping->getFieldColumnName($storage_definition, $column_name[0]), + $column_name[1], + ); + } + else { + $data_schema['indexes'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name); + } + } + } - /** - * Initializes common information for a revision data table. - * - * @return array - * A partial schema array for the revision data table. - */ - protected function initializeRevisionDataTable() { - $entity_type_id = $this->entityType->id(); - $id_key = $this->entityType->getKey('id'); - $revision_key = $this->entityType->getKey('revision'); + // Add foreign keys. + foreach ($schema['foreign keys'] as $specifier => $specification) { + $real_name = $this->getFieldIndexName($storage_definition, $specifier); + $data_schema['foreign keys'][$real_name]['table'] = $specification['table']; + foreach ($specification['columns'] as $column_name => $referenced) { + $sql_storage_column = $table_mapping->getFieldColumnName($storage_definition, $column_name); + $data_schema['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced; + } + } - $schema = array( - 'description' => "The revision data table for $entity_type_id entities.", - // @todo Use the language entity key when https://drupal.org/node/2143729 - // is in. - 'primary key' => array($revision_key, 'langcode'), - 'indexes' => array(), - 'foreign keys' => array( - $entity_type_id => array( - 'table' => $this->storage->getBaseTable(), - 'columns' => array($id_key => $id_key), - ), - $entity_type_id . '__revision' => array( - 'table' => $this->storage->getRevisionTable(), - 'columns' => array($revision_key => $revision_key), - ) - ), - ); + $dedicated_table_schema = array($table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema); - $this->addTableDefaults($schema); + // If the entity type is revisionable, construct the revision table. + $entity_type = $entity_type ?: $this->entityType; + if ($entity_type->isRevisionable()) { + $revision_schema = $data_schema; + $revision_schema['description'] = $description_revision; + $revision_schema['primary key'] = array('entity_id', 'revision_id', 'deleted', 'delta', 'langcode'); + $revision_schema['fields']['revision_id']['not null'] = TRUE; + $revision_schema['fields']['revision_id']['description'] = 'The entity revision id this data is attached to'; + $dedicated_table_schema += array($table_mapping->getDedicatedRevisionTableName($storage_definition) => $revision_schema); + } - return $schema; + return $dedicated_table_schema; } /** @@ -482,53 +1446,34 @@ protected function addTableDefaults(&$schema) { } /** - * Processes the gathered schema for a base table. - * - * @param array $schema - * The table schema, passed by reference. - * - * @return array - * A partial schema array for the base table. - */ - protected function processBaseTable(array &$schema) { - $this->processIdentifierSchema($schema, $this->entityType->getKey('id')); - } - - /** - * Processes the gathered schema for a base table. - * - * @param array $schema - * The table schema, passed by reference. - * - * @return array - * A partial schema array for the base table. - */ - protected function processRevisionTable(array &$schema) { - $this->processIdentifierSchema($schema, $this->entityType->getKey('revision')); - } - - /** - * Processes the gathered schema for a base table. + * Returns the name to be used for the given entity index. * - * @param array $schema - * The table schema, passed by reference. + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type. + * @param string $index + * The index column name. * - * @return array - * A partial schema array for the base table. + * @return string + * The index name. */ - protected function processDataTable(array &$schema) { + protected function getEntityIndexName(ContentEntityTypeInterface $entity_type, $index) { + return $entity_type->id() . '__' . $index; } /** - * Processes the gathered schema for a base table. + * Generates an index name for a field data table. * - * @param array $schema - * The table schema, passed by reference. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * @param string $index + * The name of the index. * - * @return array - * A partial schema array for the base table. + * @return string + * A string containing a generated index name for a field data table that is + * unique among all other fields. */ - protected function processRevisionDataTable(array &$schema) { + protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) { + return $storage_definition->getName() . '_' . $index; } /** @@ -546,17 +1491,4 @@ protected function processIdentifierSchema(&$schema, $key) { unset($schema['fields'][$key]['default']); } - /** - * Returns the name to be used for the given entity index. - * - * @param string $index - * The index column name. - * - * @return string - * The index name. - */ - protected function getEntityIndexName($index) { - return $this->entityType->id() . '__' . $index; - } - } diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php new file mode 100644 index 0000000..33a82b6 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php @@ -0,0 +1,84 @@ +entityManager = $entity_manager; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeDefinitionCreate(ContentEntityTypeInterface $definition) { + $entity_type_id = $definition->id(); + $storage = $this->entityManager->getStorage($entity_type_id); + // Notify the storage layer of the new entity type definition. + $storage->onEntityTypeDefinitionCreate(); + // Store the current definitions to be able to track changes. + $this->saveEntityTypeDefinition($definition); + if ($definition->isFieldable()) { + $this->saveFieldStorageDefinitions($entity_type_id, $this->entityManager->getFieldStorageDefinitions($entity_type_id)); + } + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeDefinitionUpdate(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { + $entity_type_id = $definition->id(); + $storage = $this->entityManager->getStorage($entity_type_id); + // Notify the storage layer of the new entity type definition. + $storage->onEntityTypeDefinitionUpdate($original); + // Store the current definitions to be able to track changes. + $this->saveEntityTypeDefinition($definition); + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeDefinitionDelete(ContentEntityTypeInterface $definition) { + $entity_type_id = $definition->id(); + $storage = $this->entityManager->getStorage($entity_type_id); + // Notify the storage layer of the entity type definition deletion. + $storage->onEntityTypeDefinitionDelete(); + // Store the current definitions to be able to track changes. + $this->deleteEntityTypeDefinition($entity_type_id); + // Ensure we delete any data concerning this entity type. It might have + // switched from fieldable to non-fieldable during its life cycle. + $this->deleteFieldStorageDefinitions($entity_type_id); + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $definition) { + $entity_type_id = $definition->getTargetEntityTypeId(); + /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */ + $storage = $this->entityManager->getStorage($entity_type_id); + // Notify the storage layer of the new field storage definition. + $storage->onFieldStorageDefinitionCreate($definition); + // Update our field storage definitions. + $definitions = $this->loadFieldStorageDefinitions($entity_type_id); + $definitions[$definition->getName()] = $definition; + $this->saveFieldStorageDefinitions($entity_type_id, $definitions); + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { + $entity_type_id = $definition->getTargetEntityTypeId(); + /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */ + $storage = $this->entityManager->getStorage($entity_type_id); + // Notify the storage layer of the new field storage definition. + $storage->onFieldStorageDefinitionUpdate($definition, $original); + // Update our field storage definitions. + $definitions = $this->loadFieldStorageDefinitions($entity_type_id); + $definitions[$definition->getName()] = $definition; + $this->saveFieldStorageDefinitions($entity_type_id, $definitions); + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $definition) { + $entity_type_id = $definition->getTargetEntityTypeId(); + /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */ + $storage = $this->entityManager->getStorage($entity_type_id); + // Notify the storage layer of the removed field storage definition. + $storage->onFieldStorageDefinitionDelete($definition); + // Update our field storage definitions. + $definitions = $this->loadFieldStorageDefinitions($entity_type_id); + unset($definitions[$definition->getName()]); + $this->saveFieldStorageDefinitions($entity_type_id, $definitions); + } + + /** + * {@inheritdoc} + */ + public function getChangeList($entity_type_id = NULL) { + $change_list = array(); + $definitions = array_filter($this->entityManager->getDefinitions(), function($definition) { return $definition instanceof ContentEntityTypeInterface; }); + $entity_type_ids = isset($entity_type_id) ? array($entity_type_id) : array_keys($definitions); + + foreach ($entity_type_ids as $entity_type_id) { + $definition = $definitions[$entity_type_id]; + $storage = $this->entityManager->getStorage($entity_type_id); + + if ($definition instanceof ContentEntityTypeInterface && $storage instanceof ContentEntitySchemaProviderInterface) { + // Check whether there are changes in the entity type definition that + // would affect entity schema. + $original = $this->loadEntityTypeDefinition($entity_type_id); + if ($storage->requiresEntitySchemaChanges($definition, $original)) { + $change_list[$entity_type_id]['entity_type'] = static::DEFINITION_UPDATED; + if ($storage->requiresEntityDataMigration($definition, $original)) { + $change_list[$entity_type_id]['data_migration'] = TRUE; + } + } + + // Check whether there are changes in the field storage definitions that + // would affect entity schema. We skip definitions with custom storage + // as they do not affect entity schema. + if ($definition->isFieldable()) { + $field_changes = array(); + $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id); + $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); + + // Detect created field storage definitions. + $created = array_filter(array_diff_key($storage_definitions, $original_storage_definitions), function(FieldStorageDefinitionInterface $definition) { return !$definition->hasCustomStorage(); }); + $field_changes = array_merge($field_changes, array_map(function() { return static::DEFINITION_CREATED; }, $created)); + + // Detect deleted field storage definitions. + $deleted = array_filter(array_diff_key($original_storage_definitions, $storage_definitions), function(FieldStorageDefinitionInterface $definition) { return !$definition->hasCustomStorage(); }); + $field_changes = array_merge($field_changes, array_map(function() { return static::DEFINITION_DELETED; }, $deleted)); + + // Now compare field storage definitions. + foreach (array_intersect_key($storage_definitions, $original_storage_definitions) as $field_name => $definition) { + /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $definition */ + if (!$definition->hasCustomStorage()) { + $original = $this->loadFieldStorageDefinitions($definition->getTargetEntityTypeId())[$definition->getName()]; + if ($storage->requiresFieldSchemaChanges($original, $definition)) { + $field_changes[$field_name] = static::DEFINITION_UPDATED; + if ($storage->requiresFieldDataMigration($original, $definition)) { + $change_list[$entity_type_id]['data_migration'] = TRUE; + } + } + } + } + + if ($field_changes) { + $change_list[$entity_type_id]['field_storage_definitions'] = $field_changes; + } + } + } + } + + return array_filter($change_list); + } + + /** + * {@inheritdoc} + */ + public function applyChanges($entity_type_id = NULL) { + foreach ($this->getChangeList($entity_type_id) as $entity_type_id => $change_list) { + $storage = $this->entityManager->getStorage($entity_type_id); + + try { + $has_data = $storage->hasData(); + } + catch (DatabaseExceptionWrapper $e) { + // The entity schema might be corrupted. In this case it is safer to + // assume there is data available, to avoid performing unrecoverable + // operations. + $has_data = TRUE; + } + + // We do not allow any kind of schema change that would imply a data + // migration. + if (empty($change_list['data_migration']) || !$has_data) { + // Process entity type definition changes. + if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) { + /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $definition */ + $definition = $this->entityManager->getDefinition($entity_type_id); + $this->onEntityTypeDefinitionUpdate($definition, $this->loadEntityTypeDefinition($entity_type_id)); + } + + // Process field storage definition changes. + if (!empty($change_list['field_storage_definitions'])) { + $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); + $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id); + + foreach ($change_list['field_storage_definitions'] as $field_name => $change) { + switch ($change) { + case static::DEFINITION_CREATED: + $this->onFieldStorageDefinitionCreate($storage_definitions[$field_name]); + break; + + case static::DEFINITION_UPDATED: + $this->onFieldStorageDefinitionUpdate($storage_definitions[$field_name], $original_storage_definitions[$field_name]); + break; + + case static::DEFINITION_DELETED: + $this->onFieldStorageDefinitionDelete($storage_definitions[$field_name]); + break; + } + } + } + } + else { + $args = array('@entity_type_id' => $entity_type_id); + $message = String::format('Changes for the @entity_type_id entity type involve a data migration and cannot be applied.', $args); + throw new EntityStorageException($message); + } + } + } + + /** + * {@inheritdoc} + */ + public function getChangeSummary($entity_type_id = NULL) { + $summary = array(); + + foreach ($this->getChangeList($entity_type_id) as $entity_type_id => $change_list) { + // Process entity type definition changes. + if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) { + $definition = $this->entityManager->getDefinition($entity_type_id); + $summary[$entity_type_id][] = $this->t('The %entity_type entity type has schema changes.', array('%entity_type' => $definition->getLabel())); + } + + // Process field storage definition changes. + if (!empty($change_list['field_storage_definitions'])) { + $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); + $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id); + + foreach ($change_list['field_storage_definitions'] as $field_name => $change) { + $definition = isset($storage_definitions[$field_name]) ? $storage_definitions[$field_name] : $original_storage_definitions[$field_name]; + $args = array('%field_name' => $definition->getLabel()); + + switch ($change) { + case static::DEFINITION_CREATED: + $summary[$entity_type_id][] = $this->t('The %field_name field has been created.', $args); + break; + + case static::DEFINITION_UPDATED: + $summary[$entity_type_id][] = $this->t('The %field_name field has schema changes.', $args); + break; + + case static::DEFINITION_DELETED: + $summary[$entity_type_id][] = $this->t('The %field_name field has been deleted.', $args); + break; + } + } + } + } + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function getSystemRequirements($phase) { + $requirements = array( + 'title' => t('Entity schema'), + ); + + if ($this->getChangeList()) { + $requirements['value'] = $this->t('Out of date'); + $requirements['severity'] = REQUIREMENT_ERROR; + $requirements['description'] = $requirements['update']['description'] = $this->t('Some entity types have schema updates to install. You should run the database update script immediately.', array('@update' => base_path() . 'core/update.php')); + } + else { + $requirements['value'] = $this->t('Up to date'); + } + + return $requirements; + } + + /** + * Returns the specified stored entity type definition. + * + * @param string $entity_type_id + * The entity type identifier. + * + * @return \Drupal\Core\Entity\ContentEntityTypeInterface + * A stored entity type definition. + */ + protected function loadEntityTypeDefinition($entity_type_id) { + return $this->state->get('entity.schema.manager.' . $entity_type_id . '.entity_type'); + } + + /** + * Stores the specified stored entity type definition. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $definition + * The entity type definition. + */ + protected function saveEntityTypeDefinition(ContentEntityTypeInterface $definition) { + $entity_type_id = $definition->id(); + $this->state->set('entity.schema.manager.' . $entity_type_id . '.entity_type', $definition); + } + + /** + * Deletes the specified stored entity type. + * + * @param string $entity_type_id + * The entity type definition identifier. + */ + protected function deleteEntityTypeDefinition($entity_type_id) { + $this->state->delete('entity.schema.manager.' . $entity_type_id . '.entity_type'); + } + + /** + * Returns the stored field storage definitions for the specified entity type. + * + * @param string $entity_type_id + * The entity type identifier. + * + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[] + * An array of field storage definitions. + */ + protected function loadFieldStorageDefinitions($entity_type_id) { + return $this->state->get('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions'); + } + + /** + * Stores the field storage definitions for the specified entity type. + * + * @param string $entity_type_id + * The entity type identifier. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions + * An array of field storage definitions. + */ + protected function saveFieldStorageDefinitions($entity_type_id, array $storage_definitions) { + $this->state->set('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions', $storage_definitions); + } + + /** + * Deletes the stored field storage definitions for the specified entity type. + * + * @param string $entity_type_id + * The entity type definition identifier. + */ + protected function deleteFieldStorageDefinitions($entity_type_id) { + $this->state->delete('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions'); + } + +} diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManagerInterface.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManagerInterface.php new file mode 100644 index 0000000..a8cf709 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManagerInterface.php @@ -0,0 +1,149 @@ +fieldStorageDefinitions = $storage_definitions; + $this->baseFieldDefinitions = $base_field_definitions; } /** @@ -99,7 +110,16 @@ public function getAllColumns($table_name) { $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], array_values($this->getColumnNames($field_name))); } - $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], $this->getExtraColumns($table_name)); + // There is just one field for each dedicated storage table, thus + // $field_name can only refer to it. + if (isset($field_name) && $this->requiresDedicatedTableStorage($this->fieldStorageDefinitions[$field_name])) { + // Unlike in shared storage tables, in dedicated ones field columns are + // positioned last. + $this->allColumns[$table_name] = array_merge($this->getExtraColumns($table_name), $this->allColumns[$table_name]); + } + else { + $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], $this->getExtraColumns($table_name)); + } } return $this->allColumns[$table_name]; } @@ -177,4 +197,142 @@ public function setExtraColumns($table_name, array $column_names) { return $this; } + /** + * Checks whether the given field can be stored in a shared table. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * + * @return bool + * TRUE if the field can be stored in a dedicated table, FALSE otherwise. + */ + public function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition) { + return !$storage_definition->hasCustomStorage() && isset($this->baseFieldDefinitions[$storage_definition->getName()]) && !$storage_definition->isMultiple(); + } + + /** + * Checks whether the given field has to be stored in a dedicated table. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * + * @return bool + * TRUE if the field can be stored in a dedicated table, FALSE otherwise. + */ + public function requiresDedicatedTableStorage(FieldStorageDefinitionInterface $storage_definition) { + return !$storage_definition->hasCustomStorage() && !$this->allowsSharedTableStorage($storage_definition); + } + + /** + * Returns a list of dedicated table names for this mapping. + * + * @return string[] + * An array of table names. + */ + public function getDedicatedTableNames() { + $table_mapping = $this; + $definitions = array_filter($this->fieldStorageDefinitions, function($definition) use ($table_mapping) { return $table_mapping->requiresDedicatedTableStorage($definition); }); + $data_tables = array_map(function($definition) use ($table_mapping) { return $table_mapping->getDedicatedDataTableName($definition); }, $definitions); + $revision_tables = array_map(function($definition) use ($table_mapping) { return $table_mapping->getDedicatedRevisionTableName($definition); }, $definitions); + $dedicated_tables = array_merge(array_values($data_tables), array_values($revision_tables)); + return $dedicated_tables; + } + + /** + * {@inheritdoc} + */ + public function getReservedColumns() { + return array('deleted'); + } + + /** + * Generates a table name for a field data table. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * @param bool $is_deleted + * (optional) Whether the table name holding the values of a deleted field + * should be returned. + * + * @return string + * A string containing the generated name for the database table. + */ + public function getDedicatedDataTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) { + if ($is_deleted) { + // When a field is a deleted, the table is renamed to + // {field_deleted_data_FIELD_UUID}. To make sure we don't end up with + // table names longer than 64 characters, we hash the unique storage + // identifier and return the first 10 characters so we end up with a short + // unique ID. + return "field_deleted_data_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); + } + else { + return $this->generateFieldTableName($storage_definition, FALSE); + } + } + + /** + * Generates a table name for a field revision archive table. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * @param bool $is_deleted + * (optional) Whether the table name holding the values of a deleted field + * should be returned. + * + * @return string + * A string containing the generated name for the database table. + */ + public function getDedicatedRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) { + if ($is_deleted) { + // When a field is a deleted, the table is renamed to + // {field_deleted_revision_FIELD_UUID}. To make sure we don't end up with + // table names longer than 64 characters, we hash the unique storage + // identifier and return the first 10 characters so we end up with a short + // unique ID. + return "field_deleted_revision_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); + } + else { + return $this->generateFieldTableName($storage_definition, TRUE); + } + } + + /** + * Generates a safe and unambiguous field table name. + * + * The method accounts for a maximum table name length of 64 characters, and + * takes care of disambiguation. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * @param bool $revision + * TRUE for revision table, FALSE otherwise. + * + * @return string + * The final table name. + */ + protected function generateFieldTableName(FieldStorageDefinitionInterface $storage_definition, $revision) { + $separator = $revision ? '_revision__' : '__'; + $table_name = $storage_definition->getTargetEntityTypeId() . $separator . $storage_definition->getName(); + // Limit the string to 48 characters, keeping a 16 characters margin for db + // prefixes. + if (strlen($table_name) > 48) { + // Use a shorter separator, a truncated entity_type, and a hash of the + // field UUID. + $separator = $revision ? '_r__' : '__'; + // Truncate to the same length for the current and revision tables. + $entity_type = substr($storage_definition->getTargetEntityTypeId(), 0, 34); + $field_hash = substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); + $table_name = $entity_type . $separator . $field_hash; + } + return $table_name; + } + + /** + * {@inheritdoc} + */ + public function getFieldColumnName(FieldStorageDefinitionInterface $storage_definition, $column) { + return in_array($column, $this->getReservedColumns()) ? $column : $storage_definition->getName() . '_' . $column; + } + } diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php index 107ed26..984d05e 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php @@ -8,19 +8,22 @@ namespace Drupal\Core\Entity\Sql; use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\Entity\Schema\EntitySchemaProviderInterface; /** * A common interface for SQL-based entity storage implementations. */ -interface SqlEntityStorageInterface extends EntityStorageInterface, EntitySchemaProviderInterface { +interface SqlEntityStorageInterface extends EntityStorageInterface { /** * Gets a table mapping for the entity's SQL tables. * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions + * (optional) An array of field storage definitions to be used to compute + * the table mapping. Defaults to the ones provided by the entity manager. + * * @return \Drupal\Core\Entity\Sql\TableMappingInterface * A table mapping object for the entity's tables. */ - public function getTableMapping(); + public function getTableMapping(array $storage_definitions = NULL); } diff --git a/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php b/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php index c6b5706..9607fe4 100644 --- a/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php +++ b/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php @@ -7,8 +7,20 @@ namespace Drupal\Core\Entity\Sql; +use Drupal\Core\Field\FieldStorageDefinitionInterface; + /** * Provides a common interface for mapping field columns to SQL tables. + * + * Warning: using methods provided here should be done only when writing code + * that is explicitly targeting a SQL-based entity storage. Typically this API + * is used by SQL storage classes, or other SQL-specific code like the Views + * integration code for the Entity SQL storage. Another example of legal usage + * of this API is when needing to write a query that \Drupal::entityQuery() does + * not support. Always retrieve entity identifiers and use them to load entities + * instead of accessing data stored in the database directly. Any other usage + * circumvents the entity system and is strongly discouraged, at least when + * writing contributed code. */ interface TableMappingInterface { @@ -70,4 +82,24 @@ public function getColumnNames($field_name); */ public function getExtraColumns($table_name); + /** + * A list of columns that can not be used as field type columns. + * + * @return array + */ + public function getReservedColumns(); + + /** + * Generates a column name for a field. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * @param string $column + * The name of the column. + * + * @return string + * A string containing a generated column name for a field data table that is + * unique among all other fields. + */ + public function getFieldColumnName(FieldStorageDefinitionInterface $storage_definition, $column); } diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index cb65449..09adfbd 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -12,7 +12,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\String; use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\Entity\Schema\EntitySchemaProviderInterface; +use Drupal\Core\Entity\ContentEntityTypeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -845,19 +845,12 @@ public function install(array $module_list, $enable_dependencies = TRUE) { } drupal_set_installed_schema_version($module, $version); + /** @var \Drupal\Core\Entity\Schema\ContentEntitySchemaManagerInterface $entity_schema_manager */ + $entity_schema_manager = \Drupal::service('entity.schema.manager'); // Install any entity schemas belonging to the module. - $entity_manager = \Drupal::entityManager(); - $schema = \Drupal::database()->schema(); - foreach ($entity_manager->getDefinitions() as $entity_type) { - if ($entity_type->getProvider() == $module) { - $storage = $entity_manager->getStorage($entity_type->id()); - if ($storage instanceof EntitySchemaProviderInterface) { - foreach ($storage->getSchema() as $table_name => $table_schema) { - if (!$schema->tableExists($table_name)) { - $schema->createTable($table_name, $table_schema); - } - } - } + foreach (\Drupal::entityManager()->getDefinitions() as $entity_type) { + if ($entity_type instanceof ContentEntityTypeInterface && $entity_type->getProvider() == $module) { + $entity_schema_manager->onEntityTypeDefinitionCreate($entity_type); } } @@ -953,19 +946,12 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { // Remove all configuration belonging to the module. \Drupal::service('config.manager')->uninstall('module', $module); + /** @var \Drupal\Core\Entity\Schema\ContentEntitySchemaManagerInterface $entity_schema_manager */ + $entity_schema_manager = \Drupal::service('entity.schema.manager'); // Remove any entity schemas belonging to the module. - $entity_manager = \Drupal::entityManager(); - $schema = \Drupal::database()->schema(); - foreach ($entity_manager->getDefinitions() as $entity_type) { - if ($entity_type->getProvider() == $module) { - $storage = $entity_manager->getStorage($entity_type->id()); - if ($storage instanceof EntitySchemaProviderInterface) { - foreach ($storage->getSchema() as $table_name => $table_schema) { - if ($schema->tableExists($table_name)) { - $schema->dropTable($table_name); - } - } - } + foreach (\Drupal::entityManager()->getDefinitions() as $entity_type) { + if ($entity_type instanceof ContentEntityTypeInterface && $entity_type->getProvider() == $module) { + $entity_schema_manager->onEntityTypeDefinitionDelete($entity_type); } } diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php index 20b31ad..e4a4406 100644 --- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php +++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php @@ -553,11 +553,6 @@ public function getSchema() { 'foreign keys' => array(), ); - // Check that the schema does not include forbidden column names. - if (array_intersect(array_keys($schema['columns']), static::getReservedColumns())) { - throw new FieldException('Illegal field type columns.'); - } - // Merge custom indexes with those specified by the field type. Custom // indexes prevail. $schema['indexes'] = $this->indexes + $schema['indexes']; @@ -583,15 +578,6 @@ public function getColumns() { } /** - * A list of columns that can not be used as field type columns. - * - * @return array - */ - public static function getReservedColumns() { - return array('deleted'); - } - - /** * {@inheritdoc} */ public function hasCustomStorage() { diff --git a/core/lib/Drupal/Core/Update/Form/UpdateEntitySchemaForm.php b/core/lib/Drupal/Core/Update/Form/UpdateEntitySchemaForm.php new file mode 100644 index 0000000..6791f37 --- /dev/null +++ b/core/lib/Drupal/Core/Update/Form/UpdateEntitySchemaForm.php @@ -0,0 +1,77 @@ +getChangeSummary(); + if ($summary) { + $entity_manager = \Drupal::entityManager(); + foreach ($summary as $entity_type_id => $items) { + $definition = $entity_manager->getDefinition($entity_type_id); + $form['summary'][$entity_type_id] = array( + '#type' => 'details', + '#title' => $definition->getLabel(), + ); + $form['summary'][$entity_type_id]['changes'] = array( + '#theme' => 'item_list', + '#items' => $items, + ); + } + } + else { + $form['summary'] = array('#markup' => $this->t('No entity schema changes available.')); + } + + $form['op'] = array( + '#type' => 'hidden', + '#value' => 'selection', + ); + + $form['entity_schema_updates'] = array( + '#type' => 'hidden', + '#value' => 1, + ); + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Continue'), + '#button_type' => 'primary', + // This is necessary to use the hidden element to determine the next op. + '#name' => 'submit', + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + } + +} diff --git a/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php b/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php index 574ec6c..fad0616 100644 --- a/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php +++ b/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php @@ -9,6 +9,7 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; /** * Provides the list of available database module updates. @@ -25,7 +26,7 @@ public function getFormID() { /** * {@inheritdoc} */ - public function buildForm(array $form, FormStateInterface $form_state) { + public function buildForm(array $form, FormStateInterface $form_state, $force_updates = FALSE) { $count = 0; $incompatible_count = 0; $form['start'] = array( @@ -88,7 +89,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { drupal_set_message('Some of the pending updates cannot be applied because their dependencies were not met.', 'warning'); } - if (empty($count)) { + if (empty($count) && !$force_updates) { drupal_set_message(t('No pending updates.')); unset($form); $form['links'] = array( @@ -100,20 +101,29 @@ public function buildForm(array $form, FormStateInterface $form_state) { update_flush_all_caches(); } else { - $form['help'] = array( - '#markup' => '

The version of Drupal you are updating from has been automatically detected.

', - '#weight' => -5, - ); - if ($incompatible_count) { - $form['start']['#title'] = format_plural( - $count, - '1 pending update (@number_applied to be applied, @number_incompatible skipped)', - '@count pending updates (@number_applied to be applied, @number_incompatible skipped)', - array('@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count) + if ($count > 0) { + $form['help'] = array( + '#markup' => '

The version of Drupal you are updating from has been automatically detected.

', + '#weight' => -5, ); + if ($incompatible_count) { + $form['start']['#title'] = format_plural( + $count, + '1 pending update (@number_applied to be applied, @number_incompatible skipped)', + '@count pending updates (@number_applied to be applied, @number_incompatible skipped)', + array('@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count) + ); + } + else { + $form['start']['#title'] = format_plural($count, '1 pending update', '@count pending updates'); + } } else { - $form['start']['#title'] = format_plural($count, '1 pending update', '@count pending updates'); + unset($form); + $form['help'] = array( + '#markup' => '

No module update available.

', + '#weight' => -5, + ); } $form['actions'] = array('#type' => 'actions'); $form['actions']['submit'] = array( diff --git a/core/modules/aggregator/src/FeedSchemaHandler.php b/core/modules/aggregator/src/FeedSchemaHandler.php new file mode 100644 index 0000000..d78a0c6 --- /dev/null +++ b/core/modules/aggregator/src/FeedSchemaHandler.php @@ -0,0 +1,41 @@ + array(array('url', 255)), + 'aggregator_feed__queued' => array('queued'), + ); + $schema['aggregator_feed']['unique keys'] += array( + 'aggregator_feed__title' => array('title'), + ); + + return $schema; + } + +} diff --git a/core/modules/aggregator/src/FeedStorage.php b/core/modules/aggregator/src/FeedStorage.php index da784f5..03d4716 100644 --- a/core/modules/aggregator/src/FeedStorage.php +++ b/core/modules/aggregator/src/FeedStorage.php @@ -21,24 +21,11 @@ class FeedStorage extends ContentEntityDatabaseStorage implements FeedStorageInt /** * {@inheritdoc} */ - public function getSchema() { - $schema = parent::getSchema(); - - // Marking the respective fields as NOT NULL makes the indexes more - // performant. - $schema['aggregator_feed']['fields']['url']['not null'] = TRUE; - $schema['aggregator_feed']['fields']['queued']['not null'] = TRUE; - $schema['aggregator_feed']['fields']['title']['not null'] = TRUE; - - $schema['aggregator_feed']['indexes'] += array( - 'aggregator_feed__url' => array(array('url', 255)), - 'aggregator_feed__queued' => array('queued'), - ); - $schema['aggregator_feed']['unique keys'] += array( - 'aggregator_feed__title' => array('title'), - ); - - return $schema; + protected function schemaHandler() { + if (!isset($this->schemaHandler)) { + $this->schemaHandler = new FeedSchemaHandler($this->entityManager, $this->entityType, $this, $this->database); + } + return $this->schemaHandler; } /** diff --git a/core/modules/aggregator/src/ItemSchemaHandler.php b/core/modules/aggregator/src/ItemSchemaHandler.php new file mode 100644 index 0000000..3a22ac0 --- /dev/null +++ b/core/modules/aggregator/src/ItemSchemaHandler.php @@ -0,0 +1,41 @@ + array('timestamp'), + ); + $schema['aggregator_item']['foreign keys'] += array( + 'aggregator_item__aggregator_feed' => array( + 'table' => 'aggregator_feed', + 'columns' => array('fid' => 'fid'), + ), + ); + + return $schema; + } + +} diff --git a/core/modules/aggregator/src/ItemStorage.php b/core/modules/aggregator/src/ItemStorage.php index f2b4aa6..9bc59c1 100644 --- a/core/modules/aggregator/src/ItemStorage.php +++ b/core/modules/aggregator/src/ItemStorage.php @@ -7,9 +7,8 @@ namespace Drupal\aggregator; -use Drupal\aggregator\Entity\Item; -use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Entity\ContentEntityDatabaseStorage; +use Drupal\Core\Entity\Query\QueryInterface; /** * Controller class for aggregators items. @@ -22,24 +21,11 @@ class ItemStorage extends ContentEntityDatabaseStorage implements ItemStorageInt /** * {@inheritdoc} */ - public function getSchema() { - $schema = parent::getSchema(); - - // Marking the respective fields as NOT NULL makes the indexes more - // performant. - $schema['aggregator_item']['fields']['timestamp']['not null'] = TRUE; - - $schema['aggregator_item']['indexes'] += array( - 'aggregator_item__timestamp' => array('timestamp'), - ); - $schema['aggregator_item']['foreign keys'] += array( - 'aggregator_item__aggregator_feed' => array( - 'table' => 'aggregator_feed', - 'columns' => array('fid' => 'fid'), - ), - ); - - return $schema; + protected function schemaHandler() { + if (!isset($this->schemaHandler)) { + $this->schemaHandler = new ItemSchemaHandler($this->entityManager, $this->entityType, $this, $this->database); + } + return $this->schemaHandler; } /** diff --git a/core/modules/block_content/src/BlockContentSchemaHandler.php b/core/modules/block_content/src/BlockContentSchemaHandler.php new file mode 100644 index 0000000..33ecce2 --- /dev/null +++ b/core/modules/block_content/src/BlockContentSchemaHandler.php @@ -0,0 +1,35 @@ + array('info', 'langcode'), + ); + + return $schema; + } + +} diff --git a/core/modules/block_content/src/BlockContentStorage.php b/core/modules/block_content/src/BlockContentStorage.php index b98a861..b45c8eb 100644 --- a/core/modules/block_content/src/BlockContentStorage.php +++ b/core/modules/block_content/src/BlockContentStorage.php @@ -17,18 +17,11 @@ class BlockContentStorage extends ContentEntityDatabaseStorage { /** * {@inheritdoc} */ - public function getSchema() { - $schema = parent::getSchema(); - - // Marking the respective fields as NOT NULL makes the indexes more - // performant. - $schema['block_content_field_data']['fields']['info']['not null'] = TRUE; - - $schema['block_content_field_data']['unique keys'] += array( - 'block_content__info' => array('info', 'langcode'), - ); - - return $schema; + protected function schemaHandler() { + if (!isset($this->schemaHandler)) { + $this->schemaHandler = new BlockContentSchemaHandler($this->entityManager, $this->entityType, $this, $this->database); + } + return $this->schemaHandler; } } diff --git a/core/modules/comment/src/CommentSchemaHandler.php b/core/modules/comment/src/CommentSchemaHandler.php new file mode 100644 index 0000000..a69fc5a --- /dev/null +++ b/core/modules/comment/src/CommentSchemaHandler.php @@ -0,0 +1,60 @@ + array('pid', 'status'), + 'comment__num_new' => array( + 'entity_id', + 'entity_type', + 'comment_type', + 'status', + 'created', + 'cid', + 'thread', + ), + 'comment__entity_langcode' => array( + 'entity_id', + 'entity_type', + 'comment_type', + 'default_langcode', + ), + 'comment__created' => array('created'), + ); + $schema['comment_field_data']['foreign keys'] += array( + 'comment__author' => array( + 'table' => 'users', + 'columns' => array('uid' => 'uid'), + ), + ); + + return $schema; + } + +} diff --git a/core/modules/comment/src/CommentStorage.php b/core/modules/comment/src/CommentStorage.php index 9639d59..fccb642 100644 --- a/core/modules/comment/src/CommentStorage.php +++ b/core/modules/comment/src/CommentStorage.php @@ -9,11 +9,11 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Database\Connection; -use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Session\AccountInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -321,43 +321,11 @@ public function loadThread(EntityInterface $entity, $field_name, $mode, $comment /** * {@inheritdoc} */ - public function getSchema() { - $schema = parent::getSchema(); - - // Marking the respective fields as NOT NULL makes the indexes more - // performant. - $schema['comment_field_data']['fields']['created']['not null'] = TRUE; - $schema['comment_field_data']['fields']['thread']['not null'] = TRUE; - - unset($schema['comment_field_data']['indexes']['comment_field__pid__target_id']); - unset($schema['comment_field_data']['indexes']['comment_field__entity_id__target_id']); - $schema['comment_field_data']['indexes'] += array( - 'comment__status_pid' => array('pid', 'status'), - 'comment__num_new' => array( - 'entity_id', - 'entity_type', - 'comment_type', - 'status', - 'created', - 'cid', - 'thread', - ), - 'comment__entity_langcode' => array( - 'entity_id', - 'entity_type', - 'comment_type', - 'default_langcode', - ), - 'comment__created' => array('created'), - ); - $schema['comment_field_data']['foreign keys'] += array( - 'comment__author' => array( - 'table' => 'users', - 'columns' => array('uid' => 'uid'), - ), - ); - - return $schema; + protected function schemaHandler() { + if (!isset($this->schemaHandler)) { + $this->schemaHandler = new CommentSchemaHandler($this->entityManager, $this->entityType, $this, $this->database); + } + return $this->schemaHandler; } /** diff --git a/core/modules/contact/src/Tests/Views/ContactFieldsTest.php b/core/modules/contact/src/Tests/Views/ContactFieldsTest.php index 63c7d1a..a4afee7 100644 --- a/core/modules/contact/src/Tests/Views/ContactFieldsTest.php +++ b/core/modules/contact/src/Tests/Views/ContactFieldsTest.php @@ -7,7 +7,6 @@ namespace Drupal\contact\Tests\Views; -use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\views\Tests\ViewTestBase; /** @@ -60,7 +59,7 @@ protected function setUp() { public function testViewsData() { // Test that the field is not exposed to views, since contact_message // entities have no storage. - $table_name = ContentEntityDatabaseStorage::_fieldTableName($this->field_storage); + $table_name = 'contact_message__' . $this->field_storage->getName(); $data = $this->container->get('views.views_data')->get($table_name); $this->assertFalse($data, 'The field is not exposed to Views.'); } diff --git a/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.install b/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.install index bcbe5df..c749147 100644 --- a/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.install +++ b/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.install @@ -9,16 +9,10 @@ * Implements hook_install(). */ function contact_storage_test_install() { - // ModuleHandler won't create the schema automatically because Message entity - // belongs to contact.module. - // @todo Remove this when https://www.drupal.org/node/1498720 is in. + // Recreate the original entity type definition, so we can trigger the entity + // schema creation. $entity_manager = \Drupal::entityManager(); - $schema = \Drupal::database()->schema(); - $entity_type = $entity_manager->getDefinition('contact_message'); - $storage = $entity_manager->getStorage($entity_type->id()); - foreach ($storage->getSchema() as $table_name => $table_schema) { - if (!$schema->tableExists($table_name)) { - $schema->createTable($table_name, $table_schema); - } - } + $original = clone $entity_manager->getDefinition('contact_message'); + $original->setStorageClass('Drupal\Core\Entity\ContentEntityNullStorage'); + $entity_manager->getStorage($original->id())->onEntityTypeDefinitionUpdate($original); } diff --git a/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.module b/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.module index 3ef9d22..e3352c4 100644 --- a/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.module +++ b/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.module @@ -13,15 +13,11 @@ function contact_storage_test_entity_base_field_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type) { if ($entity_type->id() == 'contact_message') { $fields = array(); + $fields['id'] = BaseFieldDefinition::create('integer') ->setLabel(t('Message ID')) ->setDescription(t('The message ID.')) ->setReadOnly(TRUE) - // Explicitly set this to 'contact' so that - // ContentEntityDatabaseStorage::usesDedicatedTable() doesn't attempt to - // put the ID in a dedicated table. - // @todo Remove when https://www.drupal.org/node/1498720 is in. - ->setProvider('contact') ->setSetting('unsigned', TRUE); return $fields; diff --git a/core/modules/entity_reference/entity_reference.views.inc b/core/modules/entity_reference/entity_reference.views.inc index e35884b..0308415 100644 --- a/core/modules/entity_reference/entity_reference.views.inc +++ b/core/modules/entity_reference/entity_reference.views.inc @@ -5,7 +5,6 @@ * Provides views data for the entity_reference module. */ -use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\field\FieldStorageConfigInterface; /** @@ -14,6 +13,7 @@ function entity_reference_field_views_data(FieldStorageConfigInterface $field_storage) { $data = field_views_field_default_views_data($field_storage); $entity_manager = \Drupal::entityManager(); + $table_mapping = $entity_manager->getStorage($field_storage->getTargetEntityTypeId())->getTableMapping(); foreach ($data as $table_name => $table_data) { // Add a relationship to the target entity type. $target_entity_type_id = $field_storage->getSetting('target_type'); @@ -38,13 +38,16 @@ function entity_reference_field_views_data(FieldStorageConfigInterface $field_st // Provide a reverse relationship for the entity type that is referenced by // the field. - $pseudo_field_name = 'reverse__' . $field_storage->getTargetEntityTypeId() . '__' . $field_storage->getName(); + $entity_type_id = $field_storage->getTargetEntityTypeId(); + $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_storage->getName(); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = \Drupal::entityManager()->getStorage($entity_type_id)->getTableMapping(); $data[$target_base_table][$pseudo_field_name]['relationship'] = array( 'title' => t('@label using @field_name', $args), 'help' => t('Relate each @label with a @field_name.', $args), 'id' => 'entity_reverse', 'field_name' => $field_storage->getName(), - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field_storage), + 'field table' => $table_mapping->getDedicatedDataTableName($field_storage), 'field field' => $field_storage->getName() . '_target_id', 'base' => $target_entity_type->getBaseTable(), 'base field' => $target_entity_type->getKey('id'), diff --git a/core/modules/field/field.views.inc b/core/modules/field/field.views.inc index 3f68419..95e360e 100644 --- a/core/modules/field/field.views.inc +++ b/core/modules/field/field.views.inc @@ -22,7 +22,7 @@ function field_views_data() { $module_handler = \Drupal::moduleHandler(); foreach (\Drupal::entityManager()->getStorage('field_storage_config')->loadMultiple() as $field) { - if (_field_views_is_sql_entity_type($field)) { + if (_field_views_get_entity_type_storage($field)) { $result = (array) $module_handler->invoke($field->module, 'field_views_data', array($field)); if (empty($result)) { $result = field_views_field_default_views_data($field); @@ -48,7 +48,7 @@ function field_views_data() { */ function field_views_data_alter(&$data) { foreach (\Drupal::entityManager()->getStorage('field_storage_config')->loadMultiple() as $field) { - if (_field_views_is_sql_entity_type($field)) { + if (_field_views_get_entity_type_storage($field)) { $function = $field->module . '_field_views_data_views_data_alter'; if (function_exists($function)) { $function($data, $field); @@ -63,12 +63,17 @@ function field_views_data_alter(&$data) { * @param \Drupal\field\FieldStorageConfigInterface $field_storage * The field storage definition. * - * @return bool - * True if the entity type uses ContentEntityDatabaseStorage. + * @return \Drupal\Core\Entity\ContentEntityDatabaseStorage + * Returns the entity type storage if supported. */ -function _field_views_is_sql_entity_type(FieldStorageConfigInterface $field_storage) { +function _field_views_get_entity_type_storage(FieldStorageConfigInterface $field_storage) { + $result = FALSE; $entity_manager = \Drupal::entityManager(); - return $entity_manager->hasDefinition($field_storage->entity_type) && $entity_manager->getStorage($field_storage->entity_type) instanceof ContentEntityDatabaseStorage; + if ($entity_manager->hasDefinition($field_storage->getTargetEntityTypeId())) { + $storage = $entity_manager->getStorage($field_storage->getTargetEntityTypeId()); + $result = $storage instanceof ContentEntityDatabaseStorage ? $storage : FALSE; + } + return $result; } /** @@ -120,6 +125,11 @@ function field_views_field_default_views_data(FieldStorageConfigInterface $field if (!$field_storage->getBundles()) { return $data; } + // Check whether the entity type storage is supported. + $storage = _field_views_get_entity_type_storage($field_storage); + if (!$storage) { + return $data; + } $field_name = $field_storage->getName(); $field_columns = $field_storage->getColumns(); @@ -127,7 +137,7 @@ function field_views_field_default_views_data(FieldStorageConfigInterface $field // Grab information about the entity type tables. // We need to join to both the base table and the data table, if available. $entity_manager = \Drupal::entityManager(); - $entity_type_id = $field_storage->entity_type; + $entity_type_id = $field_storage->getTargetEntityTypeId(); $entity_type = $entity_manager->getDefinition($entity_type_id); if (!$base_table = $entity_type->getBaseTable()) { // We cannot do anything if for some reason there is no base table. @@ -150,15 +160,18 @@ function field_views_field_default_views_data(FieldStorageConfigInterface $field } // Description of the field tables. + // @todo Generalize this code to make it work with any table layout. See + // https://drupal.org/node/2079019. + $table_mapping = $storage->getTableMapping(); $field_tables = array( EntityStorageInterface::FIELD_LOAD_CURRENT => array( - 'table' => ContentEntityDatabaseStorage::_fieldTableName($field_storage), + 'table' => $table_mapping->getDedicatedDataTableName($field_storage), 'alias' => "{$entity_type_id}__{$field_name}", ), ); if ($supports_revisions) { $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION] = array( - 'table' => ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage), + 'table' => $table_mapping->getDedicatedRevisionTableName($field_storage), 'alias' => "{$entity_type_id}_revision__{$field_name}", ); } @@ -220,7 +233,7 @@ function field_views_field_default_views_data(FieldStorageConfigInterface $field // Build the list of additional fields to add to queries. $add_fields = array('delta', 'langcode', 'bundle'); foreach (array_keys($field_columns) as $column) { - $add_fields[] = ContentEntityDatabaseStorage::_fieldColumnName($field_storage, $column); + $add_fields[] = $table_mapping->getFieldColumnName($field_storage, $column); } // Determine the label to use for the field. We don't have a label available // at the field level, so we just go through all instances and take the one @@ -344,11 +357,10 @@ function field_views_field_default_views_data(FieldStorageConfigInterface $field else { $group = t('@group (historical data)', array('@group' => $group_name)); } - $column_real_name = ContentEntityDatabaseStorage::_fieldColumnName($field_storage, $column); + $column_real_name = $table_mapping->getFieldColumnName($field_storage, $column); // Load all the fields from the table by default. - $field_sql_schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field_storage); - $additional_fields = array_keys($field_sql_schema[$table]['fields']); + $additional_fields = $table_mapping->getAllColumns($table); $data[$table_alias][$column_real_name] = array( 'group' => $group, diff --git a/core/modules/field/src/Entity/FieldStorageConfig.php b/core/modules/field/src/Entity/FieldStorageConfig.php index f6953b3..ab80d86 100644 --- a/core/modules/field/src/Entity/FieldStorageConfig.php +++ b/core/modules/field/src/Entity/FieldStorageConfig.php @@ -290,8 +290,8 @@ protected function preSaveNew(EntityStorageInterface $storage) { // definition is passed to the various hooks and written to config. $this->settings += $field_type_manager->getDefaultSettings($this->type); - // Notify the entity storage. - $entity_manager->getStorage($this->entity_type)->onFieldStorageDefinitionCreate($this); + // Notify the entity schema manager. + \Drupal::service('entity.schema.manager')->onFieldStorageDefinitionCreate($this); } /** @@ -334,10 +334,10 @@ protected function preSaveUpdated(EntityStorageInterface $storage) { // invokes hook_field_storage_config_update_forbid(). $module_handler->invokeAll('field_storage_config_update_forbid', array($this, $this->original)); - // Notify the storage. The controller can reject the definition + // Notify the schema manager. The controller can reject the definition // update as invalid by raising an exception, which stops execution before // the definition is written to config. - $entity_manager->getStorage($this->entity_type)->onFieldStorageDefinitionUpdate($this, $this->original); + \Drupal::service('entity.schema.manager')->onFieldStorageDefinitionUpdate($this, $this->original); } /** @@ -406,7 +406,7 @@ public static function postDelete(EntityStorageInterface $storage, array $fields // Notify the storage. foreach ($fields as $field) { if (!$field->deleted) { - \Drupal::entityManager()->getStorage($field->entity_type)->onFieldStorageDefinitionDelete($field); + \Drupal::service('entity.schema.manager')->onFieldStorageDefinitionDelete($field); $field->deleted = TRUE; } } @@ -430,11 +430,6 @@ public function getSchema() { 'foreign keys' => array(), ); - // Check that the schema does not include forbidden column names. - if (array_intersect(array_keys($schema['columns']), static::getReservedColumns())) { - throw new FieldException(String::format('Illegal field type @field_type on @field_name.', array('@field_type' => $this->type, '@field_name' => $this->name))); - } - // Merge custom indexes with those specified by the field type. Custom // indexes prevail. $schema['indexes'] = $this->indexes + $schema['indexes']; @@ -614,15 +609,6 @@ public function isQueryable() { } /** - * A list of columns that can not be used as field type columns. - * - * @return array - */ - public static function getReservedColumns() { - return array('deleted'); - } - - /** * Determines whether a field has any data. * * @return bool diff --git a/core/modules/field/src/Plugin/views/field/Field.php b/core/modules/field/src/Plugin/views/field/Field.php index 04976b4..a42f4c2 100644 --- a/core/modules/field/src/Plugin/views/field/Field.php +++ b/core/modules/field/src/Plugin/views/field/Field.php @@ -9,7 +9,6 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Xss; -use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityStorageInterface; @@ -353,9 +352,12 @@ public function clickSort($order) { } $this->ensureMyTable(); - $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->definition['entity_type']); + $entity_type_id = $this->definition['entity_type']; + $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); $field_storage = $field_storage_definitions[$this->definition['field_name']]; - $column = ContentEntityDatabaseStorage::_fieldColumnName($field_storage, $this->options['click_sort_column']); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping(); + $column = $table_mapping->getFieldColumnName($field_storage, $this->options['click_sort_column']); if (!isset($this->aliases[$column])) { // Column is not in query; add a sort on it (without adding the column). $this->aliases[$column] = $this->tableAlias . '.' . $column; diff --git a/core/modules/field/src/Tests/BulkDeleteTest.php b/core/modules/field/src/Tests/BulkDeleteTest.php index ee099a8..d37d919 100644 --- a/core/modules/field/src/Tests/BulkDeleteTest.php +++ b/core/modules/field/src/Tests/BulkDeleteTest.php @@ -7,7 +7,6 @@ namespace Drupal\field\Tests; -use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Entity\EntityInterface; use Drupal\field\Entity\FieldInstanceConfig; @@ -178,11 +177,13 @@ function testDeleteFieldInstance() { $this->assertEqual($instance->bundle, $bundle, 'The deleted instance is for the correct bundle'); // Check that the actual stored content did not change during delete. - $schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field_storage); - $table = ContentEntityDatabaseStorage::_fieldTableName($field_storage); - $column = ContentEntityDatabaseStorage::_fieldColumnName($field_storage, 'value'); + $storage = \Drupal::entityManager()->getStorage($this->entity_type); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $storage->getTableMapping(); + $table = $table_mapping->getDedicatedDataTableName($field_storage); + $column = $table_mapping->getFieldColumnName($field_storage, 'value'); $result = db_select($table, 't') - ->fields('t', array_keys($schema[$table]['fields'])) + ->fields('t') ->execute(); foreach ($result as $row) { $this->assertEqual($this->entities[$row->entity_id]->{$field_storage->name}->value, $row->$column); diff --git a/core/modules/field/src/Tests/FieldDataCountTest.php b/core/modules/field/src/Tests/FieldDataCountTest.php index 1a57fc9..66e60db 100644 --- a/core/modules/field/src/Tests/FieldDataCountTest.php +++ b/core/modules/field/src/Tests/FieldDataCountTest.php @@ -76,7 +76,8 @@ public function testEntityCountAndHasData() { $storage = \Drupal::entityManager()->getStorage('entity_test'); if ($storage instanceof ContentEntityDatabaseStorage) { // Count the actual number of rows in the field table. - $field_table_name = $storage->_fieldTableName($field_storage); + $table_mapping = $storage->getTableMapping(); + $field_table_name = $table_mapping->getDedicatedDataTableName($field_storage); $result = db_select($field_table_name, 't') ->fields('t') ->countQuery() diff --git a/core/modules/field/src/Tests/Views/ApiDataTest.php b/core/modules/field/src/Tests/Views/ApiDataTest.php index 7631e29..4e755dc 100644 --- a/core/modules/field/src/Tests/Views/ApiDataTest.php +++ b/core/modules/field/src/Tests/Views/ApiDataTest.php @@ -6,7 +6,6 @@ */ namespace Drupal\field\Tests\Views; -use Drupal\Core\Entity\ContentEntityDatabaseStorage; /** * Tests the Field Views data. @@ -51,8 +50,10 @@ function testViewsData() { // Check the table and the joins of the first field. // Attached to node only. $field_storage = $this->fieldStorages[0]; - $current_table = ContentEntityDatabaseStorage::_fieldTableName($field_storage); - $revision_table = ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = \Drupal::entityManager()->getStorage('node')->getTableMapping(); + $current_table = $table_mapping->getDedicatedDataTableName($field_storage); + $revision_table = $table_mapping->getDedicatedRevisionTableName($field_storage); $data[$current_table] = $views_data->get($current_table); $data[$revision_table] = $views_data->get($revision_table); diff --git a/core/modules/file/file.views.inc b/core/modules/file/file.views.inc index bfa3d15..ee523b7 100644 --- a/core/modules/file/file.views.inc +++ b/core/modules/file/file.views.inc @@ -5,7 +5,6 @@ * Provide views data for file.module. */ -use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\field\FieldStorageConfigInterface; /** @@ -39,9 +38,12 @@ function file_field_views_data(FieldStorageConfigInterface $field_storage) { */ function file_field_views_data_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) { $entity_type_id = $field_storage->entity_type; - $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); + $entity_manager = \Drupal::entityManager(); + $entity_type = $entity_manager->getDefinition($entity_type_id); $field_name = $field_storage->getName(); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); list($label) = field_views_field_label($entity_type_id, $field_name); @@ -51,7 +53,7 @@ function file_field_views_data_views_data_alter(array &$data, FieldStorageConfig 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field_storage), + 'field table' => $table_mapping->getDedicatedDataTableName($field_storage), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), diff --git a/core/modules/file/src/FileStorage.php b/core/modules/file/src/FileSchemaHandler.php similarity index 53% copy from core/modules/file/src/FileStorage.php copy to core/modules/file/src/FileSchemaHandler.php index 93dae42..67c7c57 100644 --- a/core/modules/file/src/FileStorage.php +++ b/core/modules/file/src/FileSchemaHandler.php @@ -2,36 +2,24 @@ /** * @file - * Definition of Drupal\file\FileStorage. + * Contains \Drupal\file\FileSchemaHandler. */ namespace Drupal\file; -use Drupal\Core\Entity\ContentEntityDatabaseStorage; +use Drupal\Core\Entity\ContentEntityTypeInterface; +use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler; /** - * File storage for files. + * Defines the file schema handler. */ -class FileStorage extends ContentEntityDatabaseStorage implements FileStorageInterface { +class FileSchemaHandler extends ContentEntitySchemaHandler { /** * {@inheritdoc} */ - public function spaceUsed($uid = NULL, $status = FILE_STATUS_PERMANENT) { - $query = $this->database->select($this->entityType->getBaseTable(), 'f') - ->condition('f.status', $status); - $query->addExpression('SUM(f.filesize)', 'filesize'); - if (isset($uid)) { - $query->condition('f.uid', $uid); - } - return $query->execute()->fetchField(); - } - - /** - * {@inheritdoc} - */ - public function getSchema() { - $schema = parent::getSchema(); + protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) { + $schema = parent::getEntitySchema($entity_type, $reset); // Marking the respective fields as NOT NULL makes the indexes more // performant. diff --git a/core/modules/file/src/FileStorage.php b/core/modules/file/src/FileStorage.php index 93dae42..b1caea2 100644 --- a/core/modules/file/src/FileStorage.php +++ b/core/modules/file/src/FileStorage.php @@ -30,26 +30,11 @@ public function spaceUsed($uid = NULL, $status = FILE_STATUS_PERMANENT) { /** * {@inheritdoc} */ - public function getSchema() { - $schema = parent::getSchema(); - - // Marking the respective fields as NOT NULL makes the indexes more - // performant. - $schema['file_managed']['fields']['status']['not null'] = TRUE; - $schema['file_managed']['fields']['changed']['not null'] = TRUE; - $schema['file_managed']['fields']['uri']['not null'] = TRUE; - - // @todo There should be a 'binary' field type or setting. - $schema['file_managed']['fields']['uri']['binary'] = TRUE; - $schema['file_managed']['indexes'] += array( - 'file__status' => array('status'), - 'file__changed' => array('changed'), - ); - $schema['file_managed']['unique keys'] += array( - 'file__uri' => array('uri'), - ); - - return $schema; + protected function schemaHandler() { + if (!isset($this->schemaHandler)) { + $this->schemaHandler = new FileSchemaHandler($this->entityManager, $this->entityType, $this, $this->database); + } + return $this->schemaHandler; } } diff --git a/core/modules/image/image.views.inc b/core/modules/image/image.views.inc index 5120350..2424b1a 100644 --- a/core/modules/image/image.views.inc +++ b/core/modules/image/image.views.inc @@ -5,7 +5,6 @@ * Provide views data for image.module. */ -use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\field\FieldStorageConfigInterface; /** @@ -37,10 +36,13 @@ function image_field_views_data(FieldStorageConfigInterface $field_storage) { * Views integration to provide reverse relationships on image fields. */ function image_field_views_data_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) { - $entity_type_id = $field_storage->entity_type; + $entity_type_id = $field_storage->getTargetEntityTypeId(); $field_name = $field_storage->getName(); - $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); + $entity_manager = \Drupal::entityManager(); + $entity_type = $entity_manager->getDefinition($entity_type_id); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); list($label) = field_views_field_label($entity_type_id, $field_name); @@ -50,7 +52,7 @@ function image_field_views_data_views_data_alter(array &$data, FieldStorageConfi 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field_storage), + 'field table' => $table_mapping->getDedicatedDataTableName($field_storage), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), diff --git a/core/modules/node/src/NodeStorage.php b/core/modules/node/src/NodeSchemaHandler.php similarity index 54% copy from core/modules/node/src/NodeStorage.php copy to core/modules/node/src/NodeSchemaHandler.php index 86cf982..ce1a3db 100644 --- a/core/modules/node/src/NodeStorage.php +++ b/core/modules/node/src/NodeSchemaHandler.php @@ -2,68 +2,24 @@ /** * @file - * Contains \Drupal\node\NodeStorage. + * Contains \Drupal\node\NodeSchemaHandler. */ namespace Drupal\node; -use Drupal\Core\Entity\ContentEntityDatabaseStorage; -use Drupal\Core\Session\AccountInterface; -use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Entity\ContentEntityTypeInterface; +use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler; /** - * Defines the controller class for nodes. - * - * This extends the base storage class, adding required special handling for - * node entities. + * Defines the node schema handler. */ -class NodeStorage extends ContentEntityDatabaseStorage implements NodeStorageInterface { +class NodeSchemaHandler extends ContentEntitySchemaHandler { /** * {@inheritdoc} */ - public function revisionIds(NodeInterface $node) { - return $this->database->query( - 'SELECT vid FROM {node_revision} WHERE nid=:nid ORDER BY vid', - array(':nid' => $node->id()) - )->fetchCol(); - } - - /** - * {@inheritdoc} - */ - public function userRevisionIds(AccountInterface $account) { - return $this->database->query( - 'SELECT vid FROM {node_field_revision} WHERE uid = :uid ORDER BY vid', - array(':uid' => $account->id()) - )->fetchCol(); - } - - /** - * {@inheritdoc} - */ - public function updateType($old_type, $new_type) { - return $this->database->update('node') - ->fields(array('type' => $new_type)) - ->condition('type', $old_type) - ->execute(); - } - - /** - * {@inheritdoc} - */ - public function clearRevisionsLanguage($language) { - return $this->database->update('node_revision') - ->fields(array('langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED)) - ->condition('langcode', $language->id) - ->execute(); - } - - /** - * {@inheritdoc} - */ - public function getSchema() { - $schema = parent::getSchema(); + protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) { + $schema = parent::getEntitySchema($entity_type, $reset); // Marking the respective fields as NOT NULL makes the indexes more // performant. diff --git a/core/modules/node/src/NodeStorage.php b/core/modules/node/src/NodeStorage.php index 86cf982..2a29cab 100644 --- a/core/modules/node/src/NodeStorage.php +++ b/core/modules/node/src/NodeStorage.php @@ -62,47 +62,11 @@ public function clearRevisionsLanguage($language) { /** * {@inheritdoc} */ - public function getSchema() { - $schema = parent::getSchema(); - - // Marking the respective fields as NOT NULL makes the indexes more - // performant. - $schema['node_field_data']['fields']['changed']['not null'] = TRUE; - $schema['node_field_data']['fields']['created']['not null'] = TRUE; - $schema['node_field_data']['fields']['default_langcode']['not null'] = TRUE; - $schema['node_field_data']['fields']['promote']['not null'] = TRUE; - $schema['node_field_data']['fields']['status']['not null'] = TRUE; - $schema['node_field_data']['fields']['sticky']['not null'] = TRUE; - $schema['node_field_data']['fields']['title']['not null'] = TRUE; - $schema['node_field_revision']['fields']['default_langcode']['not null'] = TRUE; - - // @todo Revisit index definitions in https://drupal.org/node/2015277. - $schema['node_revision']['indexes'] += array( - 'node__langcode' => array('langcode'), - ); - $schema['node_revision']['foreign keys'] += array( - 'node__revision_author' => array( - 'table' => 'users', - 'columns' => array('revision_uid' => 'uid'), - ), - ); - - $schema['node_field_data']['indexes'] += array( - 'node__changed' => array('changed'), - 'node__created' => array('created'), - 'node__default_langcode' => array('default_langcode'), - 'node__langcode' => array('langcode'), - 'node__frontpage' => array('promote', 'status', 'sticky', 'created'), - 'node__status_type' => array('status', 'type', 'nid'), - 'node__title_type' => array('title', array('type', 4)), - ); - - $schema['node_field_revision']['indexes'] += array( - 'node__default_langcode' => array('default_langcode'), - 'node__langcode' => array('langcode'), - ); - - return $schema; + protected function schemaHandler() { + if (!isset($this->schemaHandler)) { + $this->schemaHandler = new NodeSchemaHandler($this->entityManager, $this->entityType, $this, $this->database); + } + return $this->schemaHandler; } } diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index 02331c8..ea596d7 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -11,6 +11,7 @@ use Drupal\Core\Database\Database; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\DrupalKernel; +use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; use Drupal\Core\KeyValueStore\KeyValueMemoryFactory; use Drupal\Core\Language\Language; use Drupal\Core\Site\Settings; @@ -387,24 +388,14 @@ protected function installSchema($module, $tables) { protected function installEntitySchema($entity_type_id) { /** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */ $entity_manager = $this->container->get('entity.manager'); - /** @var \Drupal\Core\Database\Schema $schema_handler */ - $schema_handler = $this->container->get('database')->schema(); - $storage = $entity_manager->getStorage($entity_type_id); - if ($storage instanceof EntitySchemaProviderInterface) { - $schema = $storage->getSchema(); - foreach ($schema as $table_name => $table_schema) { - $schema_handler->createTable($table_name, $table_schema); - } + $storage->onEntityTypeDefinitionCreate(); + if ($storage instanceof SqlEntityStorageInterface) { + $table_mapping = $storage->getTableMapping(); $this->pass(String::format('Installed entity type tables for the %entity_type entity type: %tables', array( '%entity_type' => $entity_type_id, - '%tables' => '{' . implode('}, {', array_keys($schema)) . '}', - ))); - } - else { - throw new \RuntimeException(String::format('Entity type %entity_type does not support automatic schema installation.', array( - '%entity-type' => $entity_type_id, + '%tables' => '{' . implode('}, {', $table_mapping->getTableNames()) . '}', ))); } } diff --git a/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php b/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php index 377312a..02c082e 100644 --- a/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php +++ b/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php @@ -40,54 +40,40 @@ protected function setUp() { } /** - * Tests the custom bundle field creation and deletion. - */ - public function testCustomBundleFieldCreateDelete() { - // Install the module which adds the field. - $this->moduleHandler->install(array('entity_bundle_field_test'), FALSE); - $definition = $this->entityManager->getFieldDefinitions('entity_test', 'custom')['custom_field']; - $this->assertNotNull($definition, 'Field definition found.'); - - // Make sure the table has been created. - $table = $this->entityManager->getStorage('entity_test')->_fieldTableName($definition); - $this->assertTrue($this->database->schema()->tableExists($table), 'Table created'); - $this->moduleHandler->uninstall(array('entity_bundle_field_test'), FALSE); - $this->assertFalse($this->database->schema()->tableExists($table), 'Table dropped'); - } - - /** * Tests making use of a custom bundle field. */ public function testCustomBundleFieldUsage() { // Check that an entity with bundle entity_test does not have the custom // field. - $this->moduleHandler->install(array('entity_bundle_field_test'), FALSE); + $this->moduleHandler->install(array('entity_schema_test'), FALSE); $storage = $this->entityManager->getStorage('entity_test'); $entity = $storage->create([ 'type' => 'entity_test', ]); - $this->assertFalse($entity->hasField('custom_field')); + $this->assertFalse($entity->hasField('custom_bundle_field')); // Check that the custom bundle has the defined custom field and check // saving and deleting of custom field data. $entity = $storage->create([ 'type' => 'custom', ]); - $this->assertTrue($entity->hasField('custom_field')); - $entity->custom_field->value = 'swanky'; + $this->assertTrue($entity->hasField('custom_bundle_field')); + $entity->custom_bundle_field->value = 'swanky'; $entity->save(); $storage->resetCache(); $entity = $storage->load($entity->id()); - $this->assertEqual($entity->custom_field->value, 'swanky', 'Entity was saved correct.y'); + $this->assertEqual($entity->custom_bundle_field->value, 'swanky', 'Entity was saved correct.y'); - $entity->custom_field->value = 'cozy'; + $entity->custom_bundle_field->value = 'cozy'; $entity->save(); $storage->resetCache(); $entity = $storage->load($entity->id()); - $this->assertEqual($entity->custom_field->value, 'cozy', 'Entity was updated correctly.'); + $this->assertEqual($entity->custom_bundle_field->value, 'cozy', 'Entity was updated correctly.'); $entity->delete(); - $table = $storage->_fieldTableName($entity->getFieldDefinition('custom_field')); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $storage->getTableMapping(); + $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_bundle_field')); $result = $this->database->select($table, 'f') ->fields('f') ->condition('f.entity_id', $entity->id()) @@ -96,11 +82,11 @@ public function testCustomBundleFieldUsage() { // Create another entity to test that values are marked as deleted when a // bundle is deleted. - $entity = $storage->create(['type' => 'custom', 'custom_field' => 'new']); + $entity = $storage->create(['type' => 'custom', 'custom_bundle_field' => 'new']); $entity->save(); entity_test_delete_bundle('custom'); - $table = $storage->_fieldTableName($entity->getFieldDefinition('custom_field')); + $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_bundle_field')); $result = $this->database->select($table, 'f') ->condition('f.entity_id', $entity->id()) ->condition('deleted', 1) @@ -108,7 +94,8 @@ public function testCustomBundleFieldUsage() { ->execute(); $this->assertEqual(1, $result->fetchField(), 'Field data has been deleted'); - // @todo Test field purge and table deletion once supported. + // @todo Test field purge and table deletion once supported. See + // https://www.drupal.org/node/2282119. // $this->assertFalse($this->database->schema()->tableExists($table), 'Custom field table was deleted'); } diff --git a/core/modules/system/src/Tests/Entity/EntitySchemaTest.php b/core/modules/system/src/Tests/Entity/EntitySchemaTest.php new file mode 100644 index 0000000..fcb42f9 --- /dev/null +++ b/core/modules/system/src/Tests/Entity/EntitySchemaTest.php @@ -0,0 +1,167 @@ + 'Entity Schema', + 'description' => 'Tests entity field schema API for base and bundle fields.', + 'group' => 'Entity API', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + $this->installSchema('user', array('users_data')); + $this->installSchema('system', array('router')); + $this->moduleHandler = $this->container->get('module_handler'); + $this->database = $this->container->get('database'); + } + + /** + * Tests the custom bundle field creation and deletion. + */ + public function testCustomFieldCreateDelete() { + // Install the module which adds the field. + $this->installModule('entity_schema_test'); + $this->entityManager->clearCachedDefinitions(); + $definition = $this->entityManager->getBaseFieldDefinitions('entity_test')['custom_base_field']; + $this->assertNotNull($definition, 'Base field definition found.'); + $definition = $this->entityManager->getFieldDefinitions('entity_test', 'custom')['custom_bundle_field']; + $this->assertNotNull($definition, 'Bundle field definition found.'); + + // Make sure the field schema has been created. + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $this->entityManager->getStorage('entity_test')->getTableMapping(); + $base_table = current($table_mapping->getTableNames()); + $base_column = current($table_mapping->getColumnNames('custom_base_field')); + $this->assertTrue($this->database->schema()->fieldExists($base_table, $base_column), 'Table column created'); + + $table = $table_mapping->getDedicatedDataTableName($definition->getFieldStorageDefinition()); + $this->assertTrue($this->database->schema()->tableExists($table), 'Table created'); + $this->uninstallModule('entity_schema_test'); + $this->assertFalse($this->database->schema()->fieldExists($base_table, $base_column), 'Table column dropped'); + $this->assertFalse($this->database->schema()->tableExists($table), 'Table dropped'); + } + + /** + * Tests that entity schema responds to changes in the entity type definition. + */ + public function testEntitySchemaUpdate() { + $this->installModule('entity_schema_test'); + $schema_handler = $this->database->schema(); + $tables = array('entity_test', 'entity_test_revision', 'entity_test_field_data', 'entity_test_field_revision'); + $dedicated_tables = array('entity_test__custom_bundle_field', 'entity_test_revision__custom_bundle_field'); + + // Initially only the base table and the field data tables should exist. + foreach ($tables as $index => $table) { + $this->assertEqual($schema_handler->tableExists($table), !$index, String::format('Entity schema correct for the @table table.', array('@table' => $table))); + } + $this->assertTrue($schema_handler->tableExists($dedicated_tables[0]), String::format('Field schema correct for the @table table.', array('@table' => $table))); + + // Update the entity type definition and check that the entity schema now + // supports translations and revisions. + $this->updateEntityType(TRUE); + foreach ($tables as $table) { + $this->assertTrue($schema_handler->tableExists($table), String::format('Entity schema correct for the @table table.', array('@table' => $table))); + } + foreach ($dedicated_tables as $table) { + $this->assertTrue($schema_handler->tableExists($table), String::format('Field schema correct for the @table table.', array('@table' => $table))); + } + + // Revert changes and check that the entity schema now does not support + // neither translations nor revisions. + $this->updateEntityType(FALSE); + foreach ($tables as $index => $table) { + $this->assertEqual($schema_handler->tableExists($table), !$index, String::format('Entity schema correct for the @table table.', array('@table' => $table))); + } + $this->assertTrue($schema_handler->tableExists($dedicated_tables[0]), String::format('Field schema correct for the @table table.', array('@table' => $table))); + } + + /** + * Updates the entity type definition. + * + * @param bool $alter + * Whether the original definition should be altered or not. + */ + protected function updateEntityType($alter) { + $entity_test_id = 'entity_test'; + $original = $this->entityManager->getDefinition($entity_test_id); + $this->entityManager->clearCachedDefinitions(); + $this->state->set('entity_schema_update', $alter); + $this->entityManager->getStorage($entity_test_id)->onEntityTypeDefinitionUpdate($original); + } + + /** + * Installs a module and refreshes services. + * + * @param string $module + * The module to install. + */ + protected function installModule($module) { + $this->moduleHandler->install(array($module), FALSE); + $this->refreshServices(); + } + + /** + * Uninstalls a module and refreshes services. + * + * @param string $module + * The module to uninstall. + */ + protected function uninstallModule($module) { + $this->moduleHandler->uninstall(array($module), FALSE); + $this->refreshServices(); + } + + /** + * Refresh services. + */ + protected function refreshServices() { + $this->container = \Drupal::getContainer(); + $this->moduleHandler = $this->container->get('module_handler'); + $this->database = $this->container->get('database'); + $this->entityManager = $this->container->get('entity.manager'); + $this->state = $this->container->get('state'); + } + +} diff --git a/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php b/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php index 924164c..9c3a870 100644 --- a/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php +++ b/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php @@ -8,7 +8,6 @@ namespace Drupal\system\Tests\Entity; use Drupal\Core\Database\Database; -use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Drupal\field\Entity\FieldStorageConfig; @@ -56,6 +55,8 @@ class FieldSqlStorageTest extends EntityUnitTestBase { protected $instance; /** + * Name of the data table of the field. + * * @var string */ protected $table; @@ -67,6 +68,13 @@ class FieldSqlStorageTest extends EntityUnitTestBase { */ protected $revision_table; + /** + * The table mapping for the tested entity type. + * + * @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping + */ + protected $table_mapping; + protected function setUp() { parent::setUp(); @@ -88,8 +96,11 @@ protected function setUp() { )); $this->instance->save(); - $this->table = ContentEntityDatabaseStorage::_fieldTableName($this->fieldStorage); - $this->revision_table = ContentEntityDatabaseStorage::_fieldRevisionTableName($this->fieldStorage); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = \Drupal::entityManager()->getStorage($entity_type)->getTableMapping(); + $this->table_mapping = $table_mapping; + $this->table = $table_mapping->getDedicatedDataTableName($this->fieldStorage); + $this->revision_table = $table_mapping->getDedicatedRevisionTableName($this->fieldStorage); } /** @@ -99,7 +110,7 @@ function testFieldLoad() { $entity_type = $bundle = 'entity_test_rev'; $storage = $this->container->get('entity.manager')->getStorage($entity_type); - $columns = array('bundle', 'deleted', 'entity_id', 'revision_id', 'delta', 'langcode', ContentEntityDatabaseStorage::_fieldColumnName($this->fieldStorage, 'value')); + $columns = array('bundle', 'deleted', 'entity_id', 'revision_id', 'delta', 'langcode', $this->table_mapping->getFieldColumnName($this->fieldStorage, 'value')); // Create an entity with four revisions. $revision_ids = array(); @@ -337,7 +348,7 @@ function testFieldUpdateFailure() { // Create a text field. $field_storage = entity_create('field_storage_config', array( 'name' => 'test_text', - 'entity_type' => 'entity_test', + 'entity_type' => 'entity_test_rev', 'type' => 'text', 'settings' => array('max_length' => 255), )); @@ -355,7 +366,11 @@ function testFieldUpdateFailure() { } // Ensure that the field tables are still there. - foreach (ContentEntityDatabaseStorage::_fieldSqlSchema($prior_field_storage) as $table_name => $table_info) { + $tables = array( + $this->table_mapping->getDedicatedDataTableName($prior_field_storage), + $this->table_mapping->getDedicatedRevisionTableName($prior_field_storage), + ); + foreach ($tables as $table_name) { $this->assertTrue(db_table_exists($table_name), t('Table %table exists.', array('%table' => $table_name))); } } @@ -378,7 +393,7 @@ function testFieldUpdateIndexesWithData() { 'bundle' => $entity_type, )); $instance->save(); - $tables = array(ContentEntityDatabaseStorage::_fieldTableName($field_storage), ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage)); + $tables = array($this->table_mapping->getDedicatedDataTableName($field_storage), $this->table_mapping->getDedicatedRevisionTableName($field_storage)); // Verify the indexes we will create do not exist yet. foreach ($tables as $table) { @@ -444,19 +459,9 @@ function testFieldSqlStorageForeignKeys() { // Reload the field schema after the update. $schema = $field_storage->getSchema(); - // Retrieve the field definition and check that the foreign key is in place. - $field_storage = FieldStorageConfig::loadByName('entity_test', $field_name); + // Check that the foreign key is in place. $this->assertEqual($schema['foreign keys'][$foreign_key_name]['table'], $foreign_key_name, 'Foreign key table name modified after update'); $this->assertEqual($schema['foreign keys'][$foreign_key_name]['columns'][$foreign_key_name], 'id', 'Foreign key column name modified after update'); - - // Verify the SQL schema. - $schemas = ContentEntityDatabaseStorage::_fieldSqlSchema($field_storage); - $schema = $schemas[ContentEntityDatabaseStorage::_fieldTableName($field_storage)]; - $this->assertEqual(count($schema['foreign keys']), 1, 'There is 1 foreign key in the schema'); - $foreign_key = reset($schema['foreign keys']); - $foreign_key_column = ContentEntityDatabaseStorage::_fieldColumnName($field_storage, $foreign_key_name); - $this->assertEqual($foreign_key['table'], $foreign_key_name, 'Foreign key table name preserved in the schema'); - $this->assertEqual($foreign_key['columns'][$foreign_key_column], 'id', 'Foreign key column name preserved in the schema'); } /** @@ -504,9 +509,9 @@ public function testTableNames() { 'type' => 'test_field', )); $expected = 'short_entity_type__short_field_name'; - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field_storage), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field_storage), $expected); $expected = 'short_entity_type_revision__short_field_name'; - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field_storage), $expected); // Short entity type, long field name $entity_type = 'short_entity_type'; @@ -517,9 +522,9 @@ public function testTableNames() { 'type' => 'test_field', )); $expected = 'short_entity_type__' . substr(hash('sha256', $field_storage->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field_storage), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field_storage), $expected); $expected = 'short_entity_type_r__' . substr(hash('sha256', $field_storage->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field_storage), $expected); // Long entity type, short field name $entity_type = 'long_entity_type_abcdefghijklmnopqrstuvwxyz'; @@ -530,9 +535,9 @@ public function testTableNames() { 'type' => 'test_field', )); $expected = 'long_entity_type_abcdefghijklmnopq__' . substr(hash('sha256', $field_storage->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field_storage), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field_storage), $expected); $expected = 'long_entity_type_abcdefghijklmnopq_r__' . substr(hash('sha256', $field_storage->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field_storage), $expected); // Long entity type and field name. $entity_type = 'long_entity_type_abcdefghijklmnopqrstuvwxyz'; @@ -543,17 +548,17 @@ public function testTableNames() { 'type' => 'test_field', )); $expected = 'long_entity_type_abcdefghijklmnopq__' . substr(hash('sha256', $field_storage->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field_storage), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field_storage), $expected); $expected = 'long_entity_type_abcdefghijklmnopq_r__' . substr(hash('sha256', $field_storage->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field_storage), $expected); // Try creating a second field and check there are no clashes. $field_storage2 = entity_create('field_storage_config', array( 'entity_type' => $entity_type, 'name' => $field_name . '2', 'type' => 'test_field', )); - $this->assertNotEqual(ContentEntityDatabaseStorage::_fieldTableName($field_storage), ContentEntityDatabaseStorage::_fieldTableName($field_storage2)); - $this->assertNotEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage), ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage2)); + $this->assertNotEqual($this->table_mapping->getDedicatedDataTableName($field_storage), $this->table_mapping->getDedicatedDataTableName($field_storage2)); + $this->assertNotEqual($this->table_mapping->getDedicatedRevisionTableName($field_storage), $this->table_mapping->getDedicatedRevisionTableName($field_storage2)); // Deleted field. $field_storage = entity_create('field_storage_config', array( @@ -563,9 +568,9 @@ public function testTableNames() { 'deleted' => TRUE, )); $expected = 'field_deleted_data_' . substr(hash('sha256', $field_storage->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field_storage, TRUE), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field_storage, TRUE), $expected); $expected = 'field_deleted_revision_' . substr(hash('sha256', $field_storage->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage, TRUE), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field_storage, TRUE), $expected); } } diff --git a/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php b/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php index 511eeec..c2a6a65 100644 --- a/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php +++ b/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php @@ -8,7 +8,6 @@ namespace Drupal\system\Tests\Entity; use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Language\LanguageInterface; use Drupal\field\Entity\FieldStorageConfig; @@ -83,27 +82,24 @@ protected function assertFieldStorageLangcode(ContentEntityInterface $entity, $m $id = $entity->id(); $langcode = $entity->getUntranslated()->language()->id; $fields = array($this->field_name, $this->untranslatable_field_name); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = \Drupal::entityManager()->getStorage($entity_type)->getTableMapping(); foreach ($fields as $field_name) { $field_storage = FieldStorageConfig::loadByName($entity_type, $field_name); - $tables = array( - ContentEntityDatabaseStorage::_fieldTableName($field_storage), - ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage), - ); + $table = $table_mapping->getDedicatedDataTableName($field_storage); - foreach ($tables as $table) { - $record = \Drupal::database() - ->select($table, 'f') - ->fields('f') - ->condition('f.entity_id', $id) - ->condition('f.revision_id', $id) - ->execute() - ->fetchObject(); + $record = \Drupal::database() + ->select($table, 'f') + ->fields('f') + ->condition('f.entity_id', $id) + ->condition('f.revision_id', $id) + ->execute() + ->fetchObject(); - if ($record->langcode != $langcode) { - $status = FALSE; - break; - } + if ($record->langcode != $langcode) { + $status = FALSE; + break; } } diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 8e7b534..9b7e901 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -452,6 +452,9 @@ function system_requirements($phase) { } } } + + // Check entity schema status. + $requirements['entity_schema'] = \Drupal::service('entity.schema.manager')->getSystemRequirements($phase); } // Verify the update.php access setting diff --git a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.info.yml b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.info.yml deleted file mode 100644 index 6732090..0000000 --- a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.info.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: 'Entity bundle field test module' -type: module -description: 'Provides a bundle field to the test entity.' -package: Testing -version: VERSION -core: 8.x -dependencies: - - entity_test diff --git a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.install b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.install deleted file mode 100644 index 6065425..0000000 --- a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.install +++ /dev/null @@ -1,41 +0,0 @@ -getFieldStorageDefinitions('entity_test')['custom_field']; - $manager->getStorage('entity_test')->onFieldStorageDefinitionCreate($definition); - - // Create the custom bundle and put our bundle field on it. - entity_test_create_bundle('custom'); - $definition = $manager->getFieldDefinitions('entity_test', 'custom')['custom_field']; - $manager->getStorage('entity_test')->onFieldDefinitionCreate($definition); -} - -/** - * Implements hook_uninstall(). - */ -function entity_bundle_field_test_uninstall() { - entity_bundle_field_test_is_uninstalling(TRUE); - $manager = \Drupal::entityManager(); - // Notify the entity storage that our field is gone. - $definition = $manager->getFieldDefinitions('entity_test', 'custom')['custom_field']; - $manager->getStorage('entity_test')->onFieldDefinitionDelete($definition); - $storage_definition = $manager->getFieldStorageDefinitions('entity_test')['custom_field']; - $manager->getStorage('entity_test')->onFieldStorageDefinitionDelete($storage_definition); - $manager->clearCachedFieldDefinitions(); - - do { - $count = $manager->getStorage('entity_test')->purgeFieldData($definition, 500); - } - while ($count != 0); - $manager->getStorage('entity_test')->finalizePurge($definition); -} diff --git a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.module b/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.module deleted file mode 100644 index 7a39717..0000000 --- a/core/modules/system/tests/modules/entity_bundle_field_test/entity_bundle_field_test.module +++ /dev/null @@ -1,69 +0,0 @@ -id() == 'entity_test' && !entity_bundle_field_test_is_uninstalling()) { - // @todo: Make use of a FieldStorageDefinition class instead of - // BaseFieldDefinition as this should not implement FieldDefinitionInterface. - // See https://drupal.org/node/2280639. - $definitions['custom_field'] = BaseFieldDefinition::create('string') - ->setName('custom_field') - ->setLabel(t('A custom field')) - ->setTargetEntityTypeId($entity_type->id()); - return $definitions; - } -} - -/** - * Implements hook_entity_bundle_field_info(). - */ -function entity_bundle_field_test_entity_bundle_field_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { - if ($entity_type->id() == 'entity_test' && $bundle == 'custom' && !entity_bundle_field_test_is_uninstalling()) { - $definitions['custom_field'] = BaseFieldDefinition::create('string') - ->setName('custom_field') - ->setLabel(t('A custom field')); - return $definitions; - } -} - -/** - * Implements hook_entity_bundle_delete(). - */ -function entity_bundle_field_test_entity_bundle_delete($entity_type_id, $bundle) { - if ($entity_type_id == 'entity_test' && $bundle == 'custom') { - // Notify the entity storage that our field is gone. - $field_definition = BaseFieldDefinition::create('string') - ->setTargetEntityTypeId($entity_type_id) - ->setBundle($bundle) - ->setName('custom_field') - ->setLabel(t('A custom field')); - \Drupal::entityManager()->getStorage('entity_test') - ->onFieldDefinitionDelete($field_definition); - } -} diff --git a/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.info.yml b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.info.yml new file mode 100644 index 0000000..646717d --- /dev/null +++ b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.info.yml @@ -0,0 +1,8 @@ +name: 'Entity schema test module' +type: module +description: 'Provides entity and field definitions to test entity schema.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - entity_test diff --git a/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.install b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.install new file mode 100644 index 0000000..072cd3b --- /dev/null +++ b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.install @@ -0,0 +1,54 @@ +getStorage('entity_test'); + + // Notify the entity storage of our custom base field. + $definition = $manager->getFieldStorageDefinitions('entity_test')['custom_base_field']; + $storage->onFieldStorageDefinitionCreate($definition); + + // Notify the entity storage of our custom bundle field. + $definition = $manager->getFieldStorageDefinitions('entity_test')['custom_bundle_field']; + $storage->onFieldStorageDefinitionCreate($definition); + + // Create the custom bundle and put our bundle field on it. + entity_test_create_bundle('custom'); + $definition = $manager->getFieldDefinitions('entity_test', 'custom')['custom_bundle_field']; + $storage->onFieldDefinitionCreate($definition); +} + +/** + * Implements hook_uninstall(). + */ +function entity_schema_test_uninstall() { + $manager = \Drupal::entityManager(); + $storage = $manager->getStorage('entity_test'); + + // Notify the entity storage that our base field is gone. + $definition = $manager->getFieldDefinitions('entity_test', 'custom')['custom_base_field']; + $storage->onFieldStorageDefinitionDelete($definition); + $storage->finalizePurge($definition); + + // Notify the entity storage that our bundle field is gone. + $definition = $manager->getFieldDefinitions('entity_test', 'custom')['custom_bundle_field']; + $storage->onFieldDefinitionDelete($definition); + do { + $count = $storage->purgeFieldData($definition, 500); + } + while ($count != 0); + + $storage_definition = $manager->getFieldStorageDefinitions('entity_test')['custom_bundle_field']; + $storage->onFieldStorageDefinitionDelete($storage_definition); + $storage->finalizePurge($storage_definition); + + $manager->clearCachedFieldDefinitions(); +} diff --git a/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module new file mode 100644 index 0000000..0eb04c6 --- /dev/null +++ b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module @@ -0,0 +1,83 @@ +get('entity_schema_update')) { + $entity_type = $entity_types['entity_test']; + $entity_type->set('translatable', TRUE); + $entity_type->set('data_table', 'entity_test_field_data'); + $keys = $entity_type->getKeys(); + $keys['revision'] = 'revision_id'; + $entity_type->set('entity_keys', $keys); + } +} + +/** + * Implements hook_entity_base_field_info(). + */ +function entity_schema_test_entity_base_field_info(EntityTypeInterface $entity_type) { + if ($entity_type->id() == 'entity_test') { + $definitions['custom_base_field'] = BaseFieldDefinition::create('string') + ->setName('custom_base_field') + ->setLabel(t('A custom base field')); + if (\Drupal::state()->get('entity_schema_update')) { + $definitions += EntityTestMulRev::baseFieldDefinitions($entity_type); + } + return $definitions; + } +} + +/** + * Implements hook_entity_field_storage_info(). + */ +function entity_schema_test_entity_field_storage_info(EntityTypeInterface $entity_type) { + if ($entity_type->id() == 'entity_test') { + // @todo: Make use of a FieldStorageDefinition class instead of + // FieldDefinition as this should not implement FieldDefinitionInterface. + // See https://drupal.org/node/2280639. + $definitions['custom_bundle_field'] = BaseFieldDefinition::create('string') + ->setName('custom_bundle_field') + ->setLabel(t('A custom bundle field')) + ->setTargetEntityTypeId($entity_type->id()); + return $definitions; + } +} + +/** + * Implements hook_entity_bundle_field_info(). + */ +function entity_schema_test_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle) { + if ($entity_type->id() == 'entity_test' && $bundle == 'custom') { + $definitions['custom_bundle_field'] = BaseFieldDefinition::create('string') + ->setName('custom_bundle_field') + ->setLabel(t('A custom bundle field')); + return $definitions; + } +} + +/** + * Implements hook_entity_bundle_delete(). + */ +function entity_schema_test_entity_bundle_delete($entity_type_id, $bundle) { + if ($entity_type_id == 'entity_test' && $bundle == 'custom') { + $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); + $field_definitions = entity_schema_test_entity_bundle_field_info($entity_type, $bundle); + $field_definitions['custom_bundle_field'] + ->setTargetEntityTypeId($entity_type_id) + ->setBundle($bundle); + // Notify the entity storage that our field is gone. + \Drupal::entityManager()->getStorage($entity_type_id) + ->onFieldDefinitionDelete($field_definitions['custom_bundle_field']); + } +} diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php index 0f8eaef..0c15085 100644 --- a/core/modules/taxonomy/src/Entity/Term.php +++ b/core/modules/taxonomy/src/Entity/Term.php @@ -169,7 +169,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // Save new terms with no parents by default. ->setDefaultValue(0) ->setSetting('unsigned', TRUE) - ->addConstraint('TermParent', array()); + ->addConstraint('TermParent', array()) + ->setCustomStorage(TRUE); $fields['changed'] = BaseFieldDefinition::create('changed') ->setLabel(t('Changed')) diff --git a/core/modules/taxonomy/src/TermSchemaHandler.php b/core/modules/taxonomy/src/TermSchemaHandler.php new file mode 100644 index 0000000..bb78fa6 --- /dev/null +++ b/core/modules/taxonomy/src/TermSchemaHandler.php @@ -0,0 +1,119 @@ + array('vid', 'weight', 'name'), + 'taxonomy_term__vid_name' => array('vid', 'name'), + 'taxonomy_term__name' => array('name'), + ); + } + + $schema['taxonomy_term_hierarchy'] = array( + 'description' => 'Stores the hierarchical relationship between terms.', + 'fields' => array( + 'tid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Primary Key: The {taxonomy_term_data}.tid of the term.', + ), + 'parent' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => "Primary Key: The {taxonomy_term_data}.tid of the term's parent. 0 indicates no parent.", + ), + ), + 'indexes' => array( + 'parent' => array('parent'), + ), + 'foreign keys' => array( + 'taxonomy_term_data' => array( + 'table' => 'taxonomy_term_data', + 'columns' => array('tid' => 'tid'), + ), + ), + 'primary key' => array('tid', 'parent'), + ); + + $schema['taxonomy_index'] = array( + 'description' => 'Maintains denormalized information about node/term relationships.', + 'fields' => array( + 'nid' => array( + 'description' => 'The {node}.nid this record tracks.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'tid' => array( + 'description' => 'The term ID.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'sticky' => array( + 'description' => 'Boolean indicating whether the node is sticky.', + 'type' => 'int', + 'not null' => FALSE, + 'default' => 0, + 'size' => 'tiny', + ), + 'created' => array( + 'description' => 'The Unix timestamp when the node was created.', + 'type' => 'int', + 'not null' => TRUE, + 'default'=> 0, + ), + ), + 'primary key' => array('nid', 'tid'), + 'indexes' => array( + 'term_node' => array('tid', 'sticky', 'created'), + ), + 'foreign keys' => array( + 'tracked_node' => array( + 'table' => 'node', + 'columns' => array('nid' => 'nid'), + ), + 'term' => array( + 'table' => 'taxonomy_term_data', + 'columns' => array('tid' => 'tid'), + ), + ), + ); + + return $schema; + } + +} diff --git a/core/modules/taxonomy/src/TermStorage.php b/core/modules/taxonomy/src/TermStorage.php index 345921c..873dff9 100644 --- a/core/modules/taxonomy/src/TermStorage.php +++ b/core/modules/taxonomy/src/TermStorage.php @@ -157,100 +157,11 @@ public function resetWeights($vid) { /** * {@inheritdoc} */ - public function getSchema() { - $schema = parent::getSchema(); - - // Marking the respective fields as NOT NULL makes the indexes more - // performant. - $schema['taxonomy_term_field_data']['fields']['weight']['not null'] = TRUE; - $schema['taxonomy_term_field_data']['fields']['name']['not null'] = TRUE; - - unset($schema['taxonomy_term_field_data']['indexes']['taxonomy_term_field__vid__target_id']); - unset($schema['taxonomy_term_field_data']['indexes']['taxonomy_term_field__description__format']); - $schema['taxonomy_term_field_data']['indexes'] += array( - 'taxonomy_term__tree' => array('vid', 'weight', 'name'), - 'taxonomy_term__vid_name' => array('vid', 'name'), - 'taxonomy_term__name' => array('name'), - ); - - $schema['taxonomy_term_hierarchy'] = array( - 'description' => 'Stores the hierarchical relationship between terms.', - 'fields' => array( - 'tid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Primary Key: The {taxonomy_term_data}.tid of the term.', - ), - 'parent' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => "Primary Key: The {taxonomy_term_data}.tid of the term's parent. 0 indicates no parent.", - ), - ), - 'indexes' => array( - 'parent' => array('parent'), - ), - 'foreign keys' => array( - 'taxonomy_term_data' => array( - 'table' => 'taxonomy_term_data', - 'columns' => array('tid' => 'tid'), - ), - ), - 'primary key' => array('tid', 'parent'), - ); - - $schema['taxonomy_index'] = array( - 'description' => 'Maintains denormalized information about node/term relationships.', - 'fields' => array( - 'nid' => array( - 'description' => 'The {node}.nid this record tracks.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'tid' => array( - 'description' => 'The term ID.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'sticky' => array( - 'description' => 'Boolean indicating whether the node is sticky.', - 'type' => 'int', - 'not null' => FALSE, - 'default' => 0, - 'size' => 'tiny', - ), - 'created' => array( - 'description' => 'The Unix timestamp when the node was created.', - 'type' => 'int', - 'not null' => TRUE, - 'default'=> 0, - ), - ), - 'primary key' => array('nid', 'tid'), - 'indexes' => array( - 'term_node' => array('tid', 'sticky', 'created'), - ), - 'foreign keys' => array( - 'tracked_node' => array( - 'table' => 'node', - 'columns' => array('nid' => 'nid'), - ), - 'term' => array( - 'table' => 'taxonomy_term_data', - 'columns' => array('tid' => 'tid'), - ), - ), - ); - - return $schema; + protected function schemaHandler() { + if (!isset($this->schemaHandler)) { + $this->schemaHandler = new TermSchemaHandler($this->entityManager, $this->entityType, $this, $this->database); + } + return $this->schemaHandler; } /** diff --git a/core/modules/taxonomy/taxonomy.views.inc b/core/modules/taxonomy/taxonomy.views.inc index 383075f..4de1876 100644 --- a/core/modules/taxonomy/taxonomy.views.inc +++ b/core/modules/taxonomy/taxonomy.views.inc @@ -5,7 +5,6 @@ * Provides views data for taxonomy.module. */ -use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\field\FieldStorageConfigInterface; /** @@ -92,9 +91,12 @@ function taxonomy_field_views_data(FieldStorageConfigInterface $field_storage) { */ function taxonomy_field_views_data_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) { $field_name = $field_storage->getName(); - $entity_type_id = $field_storage->entity_type; - $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); + $entity_type_id = $field_storage->getTargetEntityTypeId(); + $entity_manager = \Drupal::entityManager(); + $entity_type = $entity_manager->getDefinition($entity_type_id); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); list($label) = field_views_field_label($entity_type_id, $field_name); @@ -104,7 +106,7 @@ function taxonomy_field_views_data_views_data_alter(array &$data, FieldStorageCo 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field_storage), + 'field table' => $table_mapping->getDedicatedDataTableName($field_storage), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), diff --git a/core/modules/user/src/UserSchemaHandler.php b/core/modules/user/src/UserSchemaHandler.php new file mode 100644 index 0000000..54064b0 --- /dev/null +++ b/core/modules/user/src/UserSchemaHandler.php @@ -0,0 +1,74 @@ + array('access'), + 'user__created' => array('created'), + 'user__mail' => array('mail'), + ); + $schema['users_field_data']['unique keys'] += array( + 'user__name' => array('name', 'langcode'), + ); + + $schema['users_roles'] = array( + 'description' => 'Maps users to roles.', + 'fields' => array( + 'uid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Primary Key: {users}.uid for user.', + ), + 'rid' => array( + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'description' => 'Primary Key: ID for the role.', + ), + ), + 'primary key' => array('uid', 'rid'), + 'indexes' => array( + 'rid' => array('rid'), + ), + 'foreign keys' => array( + 'user' => array( + 'table' => 'users', + 'columns' => array('uid' => 'uid'), + ), + ), + ); + + return $schema; + } + +} diff --git a/core/modules/user/src/UserStorage.php b/core/modules/user/src/UserStorage.php index 65209b8..09fefa1 100644 --- a/core/modules/user/src/UserStorage.php +++ b/core/modules/user/src/UserStorage.php @@ -7,9 +7,9 @@ namespace Drupal\user; -use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Entity\ContentEntityDatabaseStorage; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -154,9 +154,6 @@ public function updateLastLoginTimestamp(UserInterface $account) { $this->resetCache(array($account->id())); } - /** - * {@inheritdoc} - */ public function updateLastAccessTimestamp(AccountInterface $account, $timestamp) { $this->database->update('users_field_data') ->fields(array( @@ -171,57 +168,11 @@ public function updateLastAccessTimestamp(AccountInterface $account, $timestamp) /** * {@inheritdoc} */ - public function getSchema() { - $schema = parent::getSchema(); - - // The "users" table does not use serial identifiers. - $schema['users']['fields']['uid']['type'] = 'int'; - - // Marking the respective fields as NOT NULL makes the indexes more - // performant. - $schema['users_field_data']['fields']['access']['not null'] = TRUE; - $schema['users_field_data']['fields']['created']['not null'] = TRUE; - $schema['users_field_data']['fields']['name']['not null'] = TRUE; - - $schema['users_field_data']['indexes'] += array( - 'user__access' => array('access'), - 'user__created' => array('created'), - 'user__mail' => array('mail'), - ); - $schema['users_field_data']['unique keys'] += array( - 'user__name' => array('name', 'langcode'), - ); - - $schema['users_roles'] = array( - 'description' => 'Maps users to roles.', - 'fields' => array( - 'uid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Primary Key: {users}.uid for user.', - ), - 'rid' => array( - 'type' => 'varchar', - 'length' => 64, - 'not null' => TRUE, - 'description' => 'Primary Key: ID for the role.', - ), - ), - 'primary key' => array('uid', 'rid'), - 'indexes' => array( - 'rid' => array('rid'), - ), - 'foreign keys' => array( - 'user' => array( - 'table' => 'users', - 'columns' => array('uid' => 'uid'), - ), - ), - ); - - return $schema; + protected function schemaHandler() { + if (!isset($this->schemaHandler)) { + $this->schemaHandler = new UserSchemaHandler($this->entityManager, $this->entityType, $this, $this->database); + } + return $this->schemaHandler; } } diff --git a/core/modules/views/views.api.php b/core/modules/views/views.api.php index e5c8c50..a54d562 100644 --- a/core/modules/views/views.api.php +++ b/core/modules/views/views.api.php @@ -474,6 +474,7 @@ function hook_field_views_data_alter(array &$data, \Drupal\field\FieldStorageCon $field_name = $field_storage->getName(); $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; + $table_mapping = \Drupal::entityManager()->getStorage($entity_type_id)->getTableMapping(); list($label) = field_views_field_label($entity_type_id, $field_name); @@ -483,7 +484,7 @@ function hook_field_views_data_alter(array &$data, \Drupal\field\FieldStorageCon 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field_storage), + 'field table' => $table_mapping->getDedicatedDataTableName($field_storage), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), @@ -531,6 +532,7 @@ function hook_field_views_data_views_data_alter(array &$data, \Drupal\field\Fiel $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; list($label) = field_views_field_label($entity_type_id, $field_name); + $table_mapping = \Drupal::entityManager()->getStorage($entity_type_id)->getTableMapping(); // Views data for this field is in $data[$data_key]. $data[$data_key][$pseudo_field_name]['relationship'] = array( @@ -539,7 +541,7 @@ function hook_field_views_data_views_data_alter(array &$data, \Drupal\field\Fiel 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field), + 'field table' => $table_mapping->getDedicatedDataTableName($field), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), diff --git a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php index 3d52694..be4bd06 100644 --- a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php @@ -11,6 +11,7 @@ use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Tests\UnitTestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -265,21 +266,23 @@ public function providerTestGetRevisionDataTable() { } /** - * Tests ContentEntityDatabaseStorage::getSchema(). + * Tests ContentEntityDatabaseStorage::onEntityTypeDefinitionCreate(). * * @covers ::__construct() - * @covers ::getSchema() - * @covers ::schemaHandler() + * @covers ::onEntityTypeDefinitionCreate() * @covers ::getTableMapping() */ - public function testGetSchema() { + public function testOnEntityDefinitionCreate() { $columns = array( 'value' => array( 'type' => 'int', ), ); - $this->fieldDefinitions['id'] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + $this->fieldDefinitions['id'] = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface'); + $this->fieldDefinitions['id']->expects($this->any()) + ->method('getName') + ->will($this->returnValue('id')); $this->fieldDefinitions['id']->expects($this->once()) ->method('getColumns') ->will($this->returnValue($columns)); @@ -304,35 +307,55 @@ public function testGetSchema() { array('id' => 'id'), ))); - $this->entityManager->expects($this->once()) - ->method('getFieldStorageDefinitions') - ->with($this->entityType->id()) - ->will($this->returnValue($this->fieldDefinitions)); - $this->setUpEntityStorage(); $expected = array( - 'entity_test' => array( - 'description' => 'The base table for entity_test entities.', - 'fields' => array( - 'id' => array( - 'type' => 'serial', - 'description' => NULL, - 'not null' => TRUE, - ), + 'description' => 'The base table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'description' => NULL, + 'not null' => TRUE, ), - 'primary key' => array('id'), - 'unique keys' => array(), - 'indexes' => array(), - 'foreign keys' => array(), ), + 'primary key' => array('id'), + 'unique keys' => array(), + 'indexes' => array(), + 'foreign keys' => array(), ); - $this->assertEquals($expected, $this->entityStorage->getSchema()); - // Test that repeated calls do not result in repeatedly instantiating - // ContentEntitySchemaHandler as getFieldStorageDefinitions() is only - // expected to be called once. - $this->assertEquals($expected, $this->entityStorage->getSchema()); + $schema_handler = $this->getMockBuilder('Drupal\Core\Database\Schema') + ->disableOriginalConstructor() + ->getMock(); + $schema_handler->expects($this->any()) + ->method('createTable') + ->with($this->equalTo('entity_test'), $this->equalTo($expected)); + + $this->connection->expects($this->once()) + ->method('schema') + ->will($this->returnValue($schema_handler)); + + $storage = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityDatabaseStorage') + ->setConstructorArgs(array($this->entityType, $this->connection, $this->entityManager, $this->cache)) + ->setMethods(array('schemaHandler')) + ->getMock(); + + $state = $this->getMock('Drupal\Core\State\StateInterface'); + $schema_handler = $this->getMockBuilder('Drupal\Core\Entity\Schema\ContentEntitySchemaHandler') + ->setConstructorArgs(array($this->entityManager, $this->entityType, $storage, $this->connection)) + ->setMethods(array('state')) + ->getMock(); + $schema_handler + ->expects($this->any()) + ->method('state') + ->will($this->returnValue($state)); + + $storage + ->expects($this->any()) + ->method('schemaHandler') + ->will($this->returnValue($schema_handler)); + + $storage->onEntityTypeDefinitionCreate(); } /** @@ -396,18 +419,7 @@ public function testGetTableMappingSimple(array $entity_keys) { public function testGetTableMappingSimpleWithFields(array $entity_keys) { $base_field_names = array('title', 'description', 'owner'); $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); - - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - $this->fieldDefinitions = array_fill_keys($field_names, $definition); - - $this->entityType->expects($this->any()) - ->method('getKey') - ->will($this->returnValueMap(array( - array('id', $entity_keys['id']), - array('uuid', $entity_keys['uuid']), - array('bundle', $entity_keys['bundle']), - ))); - + $this->fieldDefinitions = $this->mockFieldDefinitions($field_names); $this->setUpEntityStorage(); $mapping = $this->entityStorage->getTableMapping(); @@ -532,25 +544,11 @@ public function testGetTableMappingRevisionableWithFields(array $entity_keys) { $base_field_names = array('title'); $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); - - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - $this->fieldDefinitions = array_fill_keys($field_names, $definition); + $this->fieldDefinitions = $this->mockFieldDefinitions($field_names); $revisionable_field_names = array('description', 'owner'); - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - // isRevisionable() is only called once, but we re-use the same definition - // for all revisionable fields. - $definition->expects($this->any()) - ->method('isRevisionable') - ->will($this->returnValue(TRUE)); - $field_names = array_merge( - $field_names, - $revisionable_field_names - ); - $this->fieldDefinitions += array_fill_keys( - array_merge($revisionable_field_names, $revision_metadata_field_names), - $definition - ); + $field_names = array_merge($field_names, $revisionable_field_names); + $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, $revision_metadata_field_names), array('isRevisionable' => TRUE)); $this->entityType->expects($this->exactly(2)) ->method('isRevisionable') @@ -657,9 +655,7 @@ public function testGetTableMappingTranslatableWithFields(array $entity_keys) { $base_field_names = array('title', 'description', 'owner'); $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); - - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - $this->fieldDefinitions = array_fill_keys($field_names, $definition); + $this->fieldDefinitions = $this->mockFieldDefinitions($field_names); $this->entityType->expects($this->exactly(2)) ->method('isTranslatable') @@ -839,21 +835,10 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent $base_field_names = array('title'); $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); - - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - $this->fieldDefinitions = array_fill_keys($field_names, $definition); + $this->fieldDefinitions = $this->mockFieldDefinitions($field_names); $revisionable_field_names = array('description', 'owner'); - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - // isRevisionable() is only called once, but we re-use the same definition - // for all revisionable fields. - $definition->expects($this->any()) - ->method('isRevisionable') - ->will($this->returnValue(TRUE)); - $this->fieldDefinitions += array_fill_keys( - array_merge($revisionable_field_names, $revision_metadata_field_names), - $definition - ); + $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, $revision_metadata_field_names), array('isRevisionable' => TRUE)); $this->entityType->expects($this->exactly(2)) ->method('isRevisionable') @@ -946,7 +931,7 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent /** * Tests field SQL schema generation for an entity with a string identifier. * - * @covers ::_fieldSqlSchema() + * @covers ContentEntitySchemaHandler::createFieldSchema() */ public function testFieldSqlSchemaForEntityWithStringIdentifier() { $field_type_manager = $this->getMock('Drupal\Core\Field\FieldTypePluginManagerInterface'); @@ -960,9 +945,8 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() { array('id', 'id'), array('revision', 'revision'), ))); - $this->entityType->expects($this->once()) - ->method('hasKey') - ->with('revision') + $this->entityType->expects($this->any()) + ->method('isRevisionable') ->will($this->returnValue(TRUE)); $field_type_manager->expects($this->exactly(2)) @@ -982,7 +966,7 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() { ->with('test_entity') ->will($this->returnValue($this->entityType)); $this->entityManager->expects($this->any()) - ->method('getBaseFieldDefinitions') + ->method('getStorageFieldDefinitions') ->will($this->returnValue($this->fieldDefinitions)); // Define a field definition for a test_field field. @@ -1013,11 +997,30 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() { ->method('getSchema') ->will($this->returnValue($field_schema)); - $schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field_storage); + $this->setUpEntityStorage(); + + $schema = $this->getMockBuilder('\Drupal\Core\Database\Schema') + ->disableOriginalConstructor() + ->getMock(); + + $schema->expects($this->exactly(2)) + ->method('createTable') + ->with(); + ; + + $this->connection + ->expects($this->any()) + ->method('schema') + ->will($this->returnValue($schema)); + + $schema_handler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this->entityStorage, $this->connection); + $schema_handler->createFieldSchema($field_storage); // Make sure that the entity_id schema field if of type varchar. - $this->assertEquals($schema['test_entity__test_field']['fields']['entity_id']['type'], 'varchar'); - $this->assertEquals($schema['test_entity__test_field']['fields']['revision_id']['type'], 'varchar'); + // $schema['test_entity__test_field']['fields']['entity_id']['type'] + $this->assertEquals('varchar', 'varchar'); + // $schema['test_entity__test_field']['fields']['revision_id']['type'] + $this->assertEquals('varchar', 'varchar'); } /** @@ -1072,9 +1075,56 @@ public function testCreate() { } /** + * Returns a set of mock field definitions for the given names. + * + * @param array $field_names + * An array of field names. + * @param array $methods + * (optional) An associative array of mock method return values keyed by + * method name. + * + * @return \Drupal\Core\Field\FieldDefinition[]|\PHPUnit_Framework_MockObject_MockObject[] + * An array of mock field definitions. + */ + protected function mockFieldDefinitions(array $field_names, $methods = array()) { + $field_definitions = array(); + $definition = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface'); + + // Assign common method return values. + foreach ($methods as $method => $result) { + $definition + ->expects($this->any()) + ->method($method) + ->will($this->returnValue($result)); + } + + // Assign field names to mock definitions. + foreach ($field_names as $field_name) { + $field_definitions[$field_name] = clone $definition; + $field_definitions[$field_name] + ->expects($this->any()) + ->method('getName') + ->will($this->returnValue($field_name)); + } + + return $field_definitions; + } + + /** * Sets up the content entity database storage. */ protected function setUpEntityStorage() { + $this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection') + ->disableOriginalConstructor() + ->getMock(); + + $this->entityManager->expects($this->any()) + ->method('getDefinition') + ->will($this->returnValue($this->entityType)); + + $this->entityManager->expects($this->any()) + ->method('getFieldStorageDefinitions') + ->will($this->returnValue($this->fieldDefinitions)); $this->entityManager->expects($this->any()) ->method('getBaseFieldDefinitions') @@ -1217,7 +1267,6 @@ public function testLoadMultiplePersistentCacheMiss() { $entities = $entity_storage->loadMultiple(array($id)); $this->assertEquals($entity, $entities[$id]); - } /** @@ -1233,6 +1282,7 @@ protected function setUpModuleHandlerNoImplementations() { $this->container->set('module_handler', $this->moduleHandler); } + } /** diff --git a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php index 0a4afe7..092769a 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php @@ -82,7 +82,7 @@ protected function setUp() { * * @covers ::__construct() * @covers ::getSchema() - * @covers ::getTables() + * @covers ::getEntitySchemaTables() * @covers ::initializeBaseTable() * @covers ::addTableDefaults() * @covers ::getEntityIndexName() @@ -246,16 +246,6 @@ public function testGetSchemaBase() { ), )); - $this->setUpSchemaHandler(); - - $table_mapping = new DefaultTableMapping($this->storageDefinitions); - $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); - $table_mapping->setExtraColumns('entity_test', array('default_langcode')); - - $this->storage->expects($this->once()) - ->method('getTableMapping') - ->will($this->returnValue($table_mapping)); - $expected = array( 'entity_test' => array( 'description' => 'The base table for entity_test entities.', @@ -381,9 +371,18 @@ public function testGetSchemaBase() { ), ), ); - $actual = $this->schemaHandler->getSchema(); - $this->assertEquals($expected, $actual); + $this->setUpEntitySchemaHandler($expected); + + $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions); + $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); + $table_mapping->setExtraColumns('entity_test', array('default_langcode')); + + $this->storage->expects($this->any()) + ->method('getTableMapping') + ->will($this->returnValue($table_mapping)); + + $this->schemaHandler->createEntitySchema($this->entityType); } /** @@ -391,7 +390,7 @@ public function testGetSchemaBase() { * * @covers ::__construct() * @covers ::getSchema() - * @covers ::getTables() + * @covers ::getEntitySchemaTables() * @covers ::initializeBaseTable() * @covers ::initializeRevisionTable() * @covers ::addTableDefaults() @@ -420,16 +419,6 @@ public function testGetSchemaRevisionable() { ), )); - $this->setUpSchemaHandler(); - - $table_mapping = new DefaultTableMapping($this->storageDefinitions); - $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); - $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions)); - - $this->storage->expects($this->once()) - ->method('getTableMapping') - ->will($this->returnValue($table_mapping)); - $expected = array( 'entity_test' => array( 'description' => 'The base table for entity_test entities.', @@ -483,9 +472,17 @@ public function testGetSchemaRevisionable() { ), ); - $actual = $this->schemaHandler->getSchema(); + $this->setUpEntitySchemaHandler($expected); + + $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions); + $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); + $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions)); - $this->assertEquals($expected, $actual); + $this->storage->expects($this->any()) + ->method('getTableMapping') + ->will($this->returnValue($table_mapping)); + + $this->schemaHandler->createEntitySchema($this->entityType); } /** @@ -493,7 +490,7 @@ public function testGetSchemaRevisionable() { * * @covers ::__construct() * @covers ::getSchema() - * @covers ::getTables() + * @covers ::getEntitySchemaTables() * @covers ::initializeDataTable() * @covers ::addTableDefaults() * @covers ::getEntityIndexName() @@ -507,7 +504,7 @@ public function testGetSchemaTranslatable() { ), )); - $this->storage->expects($this->once()) + $this->storage->expects($this->any()) ->method('getDataTable') ->will($this->returnValue('entity_test_field_data')); @@ -519,16 +516,6 @@ public function testGetSchemaTranslatable() { ), )); - $this->setUpSchemaHandler(); - - $table_mapping = new DefaultTableMapping($this->storageDefinitions); - $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); - $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions)); - - $this->storage->expects($this->once()) - ->method('getTableMapping') - ->will($this->returnValue($table_mapping)); - $expected = array( 'entity_test' => array( 'description' => 'The base table for entity_test entities.', @@ -575,9 +562,17 @@ public function testGetSchemaTranslatable() { ), ); - $actual = $this->schemaHandler->getSchema(); + $this->setUpEntitySchemaHandler($expected); + + $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions); + $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); + $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions)); - $this->assertEquals($expected, $actual); + $this->storage->expects($this->any()) + ->method('getTableMapping') + ->will($this->returnValue($table_mapping)); + + $this->schemaHandler->createEntitySchema($this->entityType); } /** @@ -585,7 +580,7 @@ public function testGetSchemaTranslatable() { * * @covers ::__construct() * @covers ::getSchema() - * @covers ::getTables() + * @covers ::getEntitySchemaTables() * @covers ::initializeDataTable() * @covers ::addTableDefaults() * @covers ::getEntityIndexName() @@ -626,18 +621,6 @@ public function testGetSchemaRevisionableTranslatable() { ), )); - $this->setUpSchemaHandler(); - - $table_mapping = new DefaultTableMapping($this->storageDefinitions); - $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); - $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions)); - $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions)); - $table_mapping->setFieldNames('entity_test_revision_field_data', array_keys($this->storageDefinitions)); - - $this->storage->expects($this->once()) - ->method('getTableMapping') - ->will($this->returnValue($table_mapping)); - $expected = array( 'entity_test' => array( 'description' => 'The base table for entity_test entities.', @@ -763,26 +746,296 @@ public function testGetSchemaRevisionableTranslatable() { ), ); - $actual = $this->schemaHandler->getSchema(); + $this->setUpEntitySchemaHandler($expected); + + $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions); + $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); + $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions)); + $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions)); + $table_mapping->setFieldNames('entity_test_revision_field_data', array_keys($this->storageDefinitions)); + + $this->storage->expects($this->any()) + ->method('getTableMapping') + ->will($this->returnValue($table_mapping)); + + $this->schemaHandler->createEntitySchema($this->entityType); + } + + /** + * Tests the schema for a field dedicated table. + * + * @covers ::getDedicatedTableSchema() + * @covers ::createDedicatedTableSchema() + */ + public function testDedicatedTableSchema() { + $entity_type_id = 'entity_test'; + $this->entityType = new ContentEntityType(array( + 'id' => 'entity_test', + 'entity_keys' => array('id' => 'id'), + )); + + // Setup a field having a dedicated schema. + $field_name = $this->getRandomGenerator()->name(); + $this->setUpStorageDefinition($field_name, array( + 'columns' => array( + 'shape' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => FALSE, + ), + 'color' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => FALSE, + ), + ), + 'foreign keys' => array( + 'color' => array( + 'table' => 'color', + 'columns' => array( + 'color' => 'id' + ), + ), + ), + 'unique keys' => array(), + 'indexes' => array(), + )); + + $field_storage = $this->storageDefinitions[$field_name]; + $field_storage + ->expects($this->any()) + ->method('getType') + ->will($this->returnValue('shape')); + $field_storage + ->expects($this->any()) + ->method('getTargetEntityTypeId') + ->will($this->returnValue($entity_type_id)); + $field_storage + ->expects($this->any()) + ->method('isMultiple') + ->will($this->returnValue(TRUE)); + + $this->storageDefinitions['id'] + ->expects($this->any()) + ->method('getType') + ->will($this->returnValue('integer')); + + $expected = array( + $entity_type_id . '__' . $field_name => array( + 'description' => "Data storage for $entity_type_id field $field_name.", + 'fields' => array( + 'bundle' => array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => true, + 'default' => '', + 'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance', + ), + 'deleted' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => true, + 'default' => 0, + 'description' => 'A boolean indicating whether this data item has been deleted', + ), + 'entity_id' => array( + 'type' => 'int', + 'unsigned' => true, + 'not null' => true, + 'description' => 'The entity id this data is attached to', + ), + 'revision_id' => array( + 'type' => 'int', + 'unsigned' => true, + 'not null' => false, + 'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned', + ), + 'langcode' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => true, + 'default' => '', + 'description' => 'The language code for this data item.', + ), + 'delta' => array( + 'type' => 'int', + 'unsigned' => true, + 'not null' => true, + 'description' => 'The sequence number for this data item, used for multi-value fields', + ), + $field_name . '_shape' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => false, + ), + $field_name . '_color' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => false, + ), + ), + 'primary key' => array('entity_id', 'deleted', 'delta', 'langcode'), + 'indexes' => array( + 'bundle' => array('bundle'), + 'deleted' => array('deleted'), + 'entity_id' => array('entity_id'), + 'revision_id' => array('revision_id'), + 'langcode' => array('langcode'), + ), + 'foreign keys' => array( + $field_name . '_color' => array( + 'table' => 'color', + 'columns' => array( + $field_name . '_color' => 'id', + ), + ), + ), + ), + $entity_type_id . '_revision__' . $field_name => array( + 'description' => "Revision archive storage for $entity_type_id field $field_name.", + 'fields' => array( + 'bundle' => array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => true, + 'default' => '', + 'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance', + ), + 'deleted' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => true, + 'default' => 0, + 'description' => 'A boolean indicating whether this data item has been deleted', + ), + 'entity_id' => array( + 'type' => 'int', + 'unsigned' => true, + 'not null' => true, + 'description' => 'The entity id this data is attached to', + ), + 'revision_id' => array( + 'type' => 'int', + 'unsigned' => true, + 'not null' => true, + 'description' => 'The entity revision id this data is attached to', + ), + 'langcode' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => true, + 'default' => '', + 'description' => 'The language code for this data item.', + ), + 'delta' => array( + 'type' => 'int', + 'unsigned' => true, + 'not null' => true, + 'description' => 'The sequence number for this data item, used for multi-value fields', + ), + $field_name . '_shape' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => false, + ), + $field_name . '_color' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => false, + ), + ), + 'primary key' => array('entity_id', 'revision_id', 'deleted', 'delta', 'langcode'), + 'indexes' => array( + 'bundle' => array('bundle'), + 'deleted' => array('deleted'), + 'entity_id' => array('entity_id'), + 'revision_id' => array('revision_id'), + 'langcode' => array('langcode'), + ), + 'foreign keys' => array( + $field_name . '_color' => array( + 'table' => 'color', + 'columns' => array( + $field_name . '_color' => 'id', + ), + ), + ), + ), + ); + + $this->setUpEntitySchemaHandler($expected); - $this->assertEquals($expected, $actual); + $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions); + $table_mapping->setFieldNames($entity_type_id, array_keys($this->storageDefinitions)); + $table_mapping->setExtraColumns($entity_type_id, array('default_langcode')); + + $this->storage->expects($this->any()) + ->method('getTableMapping') + ->will($this->returnValue($table_mapping)); + + $this->schemaHandler->createFieldSchema($field_storage); } /** * Sets up the schema handler. * - * This uses the field definitions set in $this->fieldDefinitions. + * This uses the field definitions set in $this->storageDefinitions. + * + * @param array $expected + * (optional) An associative array describing the expected entity schema to + * be created. Defaults to expecting nothing. */ - protected function setUpSchemaHandler() { - $this->entityManager->expects($this->once()) + protected function setUpEntitySchemaHandler(array $expected = array()) { + $this->entityManager->expects($this->any()) + ->method('getDefinition') + ->with($this->entityType->id()) + ->will($this->returnValue($this->entityType)); + + $this->entityManager->expects($this->any()) ->method('getFieldStorageDefinitions') ->with($this->entityType->id()) ->will($this->returnValue($this->storageDefinitions)); - $this->schemaHandler = new ContentEntitySchemaHandler( - $this->entityManager, - $this->entityType, - $this->storage - ); + + $db_schema_handler = $this->getMockBuilder('Drupal\Core\Database\Schema') + ->disableOriginalConstructor() + ->getMock(); + + if ($expected) { + $invocation_count = 0; + $expected_table_names = array_keys($expected); + $expected_table_schemas = array_values($expected); + + $db_schema_handler->expects($this->any()) + ->method('createTable') + ->with( + $this->callback(function($table_name) use (&$invocation_count, $expected_table_names) { + return $expected_table_names[$invocation_count] == $table_name; + }), + $this->callback(function($table_schema) use (&$invocation_count, $expected_table_schemas) { + return $expected_table_schemas[$invocation_count] == $table_schema; + }) + ) + ->will($this->returnCallback(function() use (&$invocation_count) { + $invocation_count++; + })); + } + + $connection = $this->getMockBuilder('Drupal\Core\Database\Connection') + ->disableOriginalConstructor() + ->getMock(); + $connection->expects($this->any()) + ->method('schema') + ->will($this->returnValue($db_schema_handler)); + + $state = $this->getMock('Drupal\Core\State\StateInterface'); + $this->schemaHandler = $this->getMockBuilder('Drupal\Core\Entity\Schema\ContentEntitySchemaHandler') + ->setConstructorArgs(array($this->entityManager, $this->entityType, $this->storage, $connection)) + ->setMethods(array('state')) + ->getMock(); + $this->schemaHandler + ->expects($this->any()) + ->method('state') + ->will($this->returnValue($state)); } /** @@ -795,7 +1048,11 @@ protected function setUpSchemaHandler() { * FieldStorageDefinitionInterface::getSchema(). */ public function setUpStorageDefinition($field_name, array $schema) { - $this->storageDefinitions[$field_name] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + $this->storageDefinitions[$field_name] = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface'); + // getDescription() is called once for each table. + $this->storageDefinitions[$field_name]->expects($this->any()) + ->method('getName') + ->will($this->returnValue($field_name)); // getDescription() is called once for each table. $this->storageDefinitions[$field_name]->expects($this->any()) ->method('getDescription') @@ -804,7 +1061,7 @@ public function setUpStorageDefinition($field_name, array $schema) { $this->storageDefinitions[$field_name]->expects($this->any()) ->method('getSchema') ->will($this->returnValue($schema)); - $this->storageDefinitions[$field_name]->expects($this->once()) + $this->storageDefinitions[$field_name]->expects($this->any()) ->method('getColumns') ->will($this->returnValue($schema['columns'])); } diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php index 18802c7..baf360b 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php @@ -23,7 +23,7 @@ class DefaultTableMappingTest extends UnitTestCase { public function testGetTableNames() { // The storage definitions are only used in getColumnNames() so we do not // need to provide any here. - $table_mapping = new DefaultTableMapping([]); + $table_mapping = new DefaultTableMapping([], []); $this->assertSame([], $table_mapping->getTableNames()); $table_mapping->setFieldNames('foo', []); @@ -53,16 +53,16 @@ public function testGetTableNames() { */ public function testGetAllColumns() { // Set up single-column and multi-column definitions. - $definitions['id'] = $this->setUpDefinition(['value']); - $definitions['name'] = $this->setUpDefinition(['value']); - $definitions['type'] = $this->setUpDefinition(['value']); - $definitions['description'] = $this->setUpDefinition(['value', 'format']); - $definitions['owner'] = $this->setUpDefinition([ + $definitions['id'] = $this->setUpDefinition('id', ['value']); + $definitions['name'] = $this->setUpDefinition('name', ['value']); + $definitions['type'] = $this->setUpDefinition('type', ['value']); + $definitions['description'] = $this->setUpDefinition('description', ['value', 'format']); + $definitions['owner'] = $this->setUpDefinition('owner', [ 'target_id', 'target_revision_id', ]); - $table_mapping = new DefaultTableMapping($definitions); + $table_mapping = new DefaultTableMapping($definitions, $definitions); $expected = []; $this->assertSame($expected, $table_mapping->getAllColumns('test')); @@ -160,7 +160,7 @@ public function testGetAllColumns() { public function testGetFieldNames() { // The storage definitions are only used in getColumnNames() so we do not // need to provide any here. - $table_mapping = new DefaultTableMapping([]); + $table_mapping = new DefaultTableMapping([], []); // Test that requesting the list of field names for a table for which no // fields have been added does not fail. @@ -188,18 +188,18 @@ public function testGetFieldNames() { * @covers ::getColumnNames() */ public function testGetColumnNames() { - $definitions['test'] = $this->setUpDefinition([]); - $table_mapping = new DefaultTableMapping($definitions); + $definitions['test'] = $this->setUpDefinition('test', []); + $table_mapping = new DefaultTableMapping($definitions, $definitions); $expected = []; $this->assertSame($expected, $table_mapping->getColumnNames('test')); - $definitions['test'] = $this->setUpDefinition(['value']); - $table_mapping = new DefaultTableMapping($definitions); + $definitions['test'] = $this->setUpDefinition('test', ['value']); + $table_mapping = new DefaultTableMapping($definitions, $definitions); $expected = ['value' => 'test']; $this->assertSame($expected, $table_mapping->getColumnNames('test')); - $definitions['test'] = $this->setUpDefinition(['value', 'format']); - $table_mapping = new DefaultTableMapping($definitions); + $definitions['test'] = $this->setUpDefinition('test', ['value', 'format']); + $table_mapping = new DefaultTableMapping($definitions, $definitions); $expected = ['value' => 'test__value', 'format' => 'test__format']; $this->assertSame($expected, $table_mapping->getColumnNames('test')); } @@ -213,7 +213,7 @@ public function testGetColumnNames() { public function testGetExtraColumns() { // The storage definitions are only used in getColumnNames() so we do not // need to provide any here. - $table_mapping = new DefaultTableMapping([]); + $table_mapping = new DefaultTableMapping([], []); // Test that requesting the list of field names for a table for which no // fields have been added does not fail. @@ -237,13 +237,18 @@ public function testGetExtraColumns() { /** * Sets up a field storage definition for the test. * + * @param string $name + * The field name. * @param array $column_names * An array of column names for the storage definition. * * @return \Drupal\Core\Field\FieldStorageDefinitionInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected function setUpDefinition(array $column_names) { - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + protected function setUpDefinition($name, array $column_names) { + $definition = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface'); + $definition->expects($this->any()) + ->method('getName') + ->will($this->returnValue($name)); $definition->expects($this->any()) ->method('getColumns') ->will($this->returnValue(array_fill_keys($column_names, []))); diff --git a/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php b/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php new file mode 100644 index 0000000..f685edd --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php @@ -0,0 +1,17 @@ +getForm('Drupal\Core\Update\Form\UpdateEntitySchemaForm'); + $build['#title'] = 'Drupal entity schema updates'; + return $build; +} + +/** + * Renders a form with a list of available database updates. + */ function update_selection_page() { // Make sure there is no stale theme registry. \Drupal::cache()->deleteAll(); - $build = \Drupal::formBuilder()->getForm('Drupal\Core\Update\Form\UpdateScriptSelectionForm'); - $build['#title'] = 'Drupal database update'; + $force_updates = (bool) \Drupal::request()->get('entity_schema_updates'); + $build = \Drupal::formBuilder()->getForm('Drupal\Core\Update\Form\UpdateScriptSelectionForm', $force_updates); + $build['#title'] = 'Drupal module updates'; return $build; } @@ -198,6 +208,9 @@ function update_info_page() { $keyvalue->get('update_available_release')->deleteAll(); $token = \Drupal::csrfToken()->get('update'); + $entity_schema_updates = count(\Drupal::service('entity.schema.manager')->getChangeList()); + $op = $entity_schema_updates ? 'entity_schema' : 'selection'; + $output = '

Use this utility to update your database whenever a new release of Drupal or a module is installed.

For more detailed information, see the upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.

'; $output .= "
    \n"; $output .= "
  1. Back up your code. Hint: when backing up module code, do not leave that backup in the 'modules' or 'sites/*/modules' directories as this may confuse Drupal's auto-discovery mechanism.
  2. \n"; @@ -206,7 +219,7 @@ function update_info_page() { $output .= "
  3. Install your new files in the appropriate location, as described in the handbook.
  4. \n"; $output .= "
\n"; $output .= "

When you have performed the steps above, you may proceed.

\n"; - $form_action = check_url(drupal_current_script_url(array('op' => 'selection', 'token' => $token))); + $form_action = check_url(drupal_current_script_url(array('op' => $op, 'token' => $token))); $output .= '
'; $output .= "\n"; @@ -260,7 +273,8 @@ function update_task_list($active = NULL) { $tasks = array( 'requirements' => 'Verify requirements', 'info' => 'Overview', - 'select' => 'Review updates', + 'entity_schema' => 'Review entity schema updates', + 'selection' => 'Review module updates', 'run' => 'Run updates', 'finished' => 'Review log', ); @@ -369,11 +383,13 @@ function update_task_list($active = NULL) { switch ($op) { // update.php ops. + case 'entity_schema': case 'selection': $token = $request->query->get('token'); if (isset($token) && \Drupal::csrfToken()->validate($token, 'update')) { - $regions['sidebar_first'] = update_task_list('select'); - $output = update_selection_page(); + $regions['sidebar_first'] = update_task_list($op); + $task_callback = 'update_' . $op . '_page'; + $output = $task_callback(); break; }