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