diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php index 2e29857..e7cfa1f 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php @@ -226,7 +226,7 @@ public function getSchema() { */ protected function schemaHandler() { if (!isset($this->schemaHandler)) { - $this->schemaHandler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this); + $this->schemaHandler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this, $this->database); } return $this->schemaHandler; } @@ -236,14 +236,14 @@ protected function schemaHandler() { */ public function getTableMapping() { if (!isset($this->tableMapping)) { + $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId); + $base_field_definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId); + $table_mapping = new DefaultTableMapping($storage_definitions, $base_field_definitions); + $this->tableMapping = $table_mapping; - $definitions = array_filter($this->getFieldStorageDefinitions(), function (FieldStorageDefinitionInterface $definition) { - // @todo Remove the check for FieldDefinitionInterface::isMultiple() when - // multiple-value base fields are supported in - // https://drupal.org/node/2248977. - return !$definition->hasCustomStorage() && !$definition->isMultiple(); + $definitions = array_filter($storage_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); @@ -323,7 +323,7 @@ public function getTableMapping() { $this->tableMapping->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); + $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, array($this->langcodeKey)); $this->tableMapping ->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields)) // Add the denormalized 'default_langcode' field to the mapping. Its @@ -331,6 +331,25 @@ public function getTableMapping() { // "revision_table.langcode = data_table.langcode". ->setExtraColumns($this->revisionDataTable, array('default_langcode')); } + + // Add dedicated tables. + $definitions = array_filter($storage_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); + } + } } return $this->tableMapping; @@ -438,7 +457,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); @@ -955,11 +974,12 @@ protected function doLoadFieldItems($entities, $age) { // Collect impacted fields. $storage_definitions = array(); $definitions = array(); + $table_mapping = $this->getTableMapping(); foreach ($bundles as $bundle => $v) { $definitions[$bundle] = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle); foreach ($definitions[$bundle] as $field_name => $field_definition) { $storage_definition = $field_definition->getFieldStorageDefinition(); - if ($this->usesDedicatedTable($storage_definition)) { + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { $storage_definitions[$field_name] = $storage_definition; } } @@ -968,7 +988,7 @@ protected function doLoadFieldItems($entities, $age) { // Load field data. $langcodes = array_keys(language_list(LanguageInterface::STATE_ALL)); foreach ($storage_definitions as $field_name => $storage_definition) { - $table = $load_current ? static::_fieldTableName($storage_definition) : static::_fieldRevisionTableName($storage_definition); + $table = $load_current ? $table_mapping->getDedicatedDataTableName($storage_definition) : $table_mapping->getDedicatedRevisionTableName($storage_definition); // Ensure that only values having valid languages are retrieved. Since we // are loading values for multiple entities, we cannot limit the query to @@ -997,7 +1017,7 @@ protected function doLoadFieldItems($entities, $age) { // For each column declared by the field, populate the item from the // prefixed database column. foreach ($storage_definition->getColumns() as $column => $attributes) { - $column_name = static::_fieldColumnName($storage_definition, $column); + $column_name = $table_mapping->getFieldColumnName($storage_definition, $column); // Unserialize the value if specified in the column schema. $item[$column] = (!empty($attributes['serialize'])) ? unserialize($row->$column_name) : $row->$column_name; } @@ -1021,6 +1041,7 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) { $entity_type = $entity->getEntityTypeId(); $default_langcode = $entity->getUntranslated()->language()->id; $translation_langcodes = array_keys($entity->getTranslationLanguages()); + $table_mapping = $this->getTableMapping(); if (!isset($vid)) { $vid = $id; @@ -1028,11 +1049,11 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) { foreach ($this->entityManager->getFieldDefinitions($entity_type, $bundle) as $field_name => $field_definition) { $storage_definition = $field_definition->getFieldStorageDefinition(); - if (!$this->usesDedicatedTable($storage_definition)) { + if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) { continue; } - $table_name = static::_fieldTableName($storage_definition); - $revision_name = static::_fieldRevisionTableName($storage_definition); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition); // Delete and insert, rather than update, in case a value was added. if ($update) { @@ -1053,7 +1074,7 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) { $do_insert = FALSE; $columns = array('entity_id', 'revision_id', 'bundle', 'delta', 'langcode'); foreach ($storage_definition->getColumns() as $column => $attributes) { - $columns[] = static::_fieldColumnName($storage_definition, $column); + $columns[] = $table_mapping->getFieldColumnName($storage_definition, $column); } $query = $this->database->insert($table_name)->fields($columns); $revision_query = $this->database->insert($revision_name)->fields($columns); @@ -1074,7 +1095,7 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) { 'langcode' => $langcode, ); foreach ($storage_definition->getColumns() as $column => $attributes) { - $column_name = static::_fieldColumnName($storage_definition, $column); + $column_name = $table_mapping->getFieldColumnName($storage_definition, $column); // Serialize the value if specified in the column schema. $record[$column_name] = !empty($attributes['serialize']) ? serialize($item->$column) : $item->$column; } @@ -1103,13 +1124,14 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) { * {@inheritdoc} */ protected function doDeleteFieldItems(EntityInterface $entity) { + $table_mapping = $this->getTableMapping(); foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) { $storage_definition = $field_definition->getFieldStorageDefinition(); - if (!$this->usesDedicatedTable($storage_definition)) { + if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) { continue; } - $table_name = static::_fieldTableName($storage_definition); - $revision_name = static::_fieldRevisionTableName($storage_definition); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition); $this->database->delete($table_name) ->condition('entity_id', $entity->id()) ->execute(); @@ -1125,12 +1147,13 @@ protected function doDeleteFieldItems(EntityInterface $entity) { protected function doDeleteFieldItemsRevision(EntityInterface $entity) { $vid = $entity->getRevisionId(); if (isset($vid)) { + $table_mapping = $this->getTableMapping(); foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) { $storage_definition = $field_definition->getFieldStorageDefinition(); - if (!$this->usesDedicatedTable($storage_definition)) { + if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) { continue; } - $revision_name = static::_fieldRevisionTableName($storage_definition); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition); $this->database->delete($revision_name) ->condition('entity_id', $entity->id()) ->condition('revision_id', $vid) @@ -1140,151 +1163,62 @@ protected function doDeleteFieldItemsRevision(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 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. + if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) { + $this->tableMapping = NULL; } + $this->schemaHandler()->createFieldSchema($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 = static::_fieldTableName($original); - $revision_table = static::_fieldRevisionTableName($original); - - $schema = $storage_definition->getSchema(); - $original_schema = $original->getSchema(); - - foreach ($original_schema['indexes'] as $name => $columns) { - if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) { - $real_name = static::_fieldIndexName($storage_definition, $name); - $this->database->schema()->dropIndex($table, $real_name); - $this->database->schema()->dropIndex($revision_table, $real_name); - } - } - $table = static::_fieldTableName($storage_definition); - $revision_table = static::_fieldRevisionTableName($storage_definition); - foreach ($schema['indexes'] as $name => $columns) { - if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) { - $real_name = static::_fieldIndexName($storage_definition, $name); - $real_columns = array(); - foreach ($columns as $column_name) { - // Indexes can be specified as either a column name or an array with - // column name and length. Allow for either case. - if (is_array($column_name)) { - $real_columns[] = array( - static::_fieldColumnName($storage_definition, $column_name[0]), - $column_name[1], - ); - } - else { - $real_columns[] = static::_fieldColumnName($storage_definition, $column_name); - } - } - $this->database->schema()->addIndex($table, $real_name, $real_columns); - $this->database->schema()->addIndex($revision_table, $real_name, $real_columns); - } - } - } + $this->schemaHandler()->updateFieldSchema($storage_definition, $original); } /** * {@inheritdoc} */ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { - // Mark all data associated with the field for deletion. - $table = static::_fieldTableName($storage_definition); - $revision_table = static::_fieldRevisionTableName($storage_definition); - $this->database->update($table) - ->fields(array('deleted' => 1)) - ->execute(); + $table_mapping = $this->getTableMapping(); + + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + // Mark all data associated with the field for deletion. + $table = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition); + $this->database->update($table) + ->fields(array('deleted' => 1)) + ->execute(); + } - // Move the table to a unique name while the table contents are being - // deleted. - $new_table = static::_fieldTableName($storage_definition, TRUE); - $revision_new_table = static::_fieldRevisionTableName($storage_definition, TRUE); - $this->database->schema()->renameTable($table, $new_table); - $this->database->schema()->renameTable($revision_table, $revision_new_table); + // Update the field schema. + $this->schemaHandler()->markFieldSchemaAsDeleted($storage_definition); } /** * {@inheritdoc} */ public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) { + $table_mapping = $this->getTableMapping(); $storage_definition = $field_definition->getFieldStorageDefinition(); - $table_name = static::_fieldTableName($storage_definition); - $revision_name = static::_fieldRevisionTableName($storage_definition); - // Mark field data as deleted. - $this->database->update($table_name) - ->fields(array('deleted' => 1)) - ->condition('bundle', $field_definition->getBundle()) - ->execute(); - $this->database->update($revision_name) - ->fields(array('deleted' => 1)) - ->condition('bundle', $field_definition->getBundle()) - ->execute(); + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition); + $this->database->update($table_name) + ->fields(array('deleted' => 1)) + ->condition('bundle', $field_definition->getBundle()) + ->execute(); + $this->database->update($revision_name) + ->fields(array('deleted' => 1)) + ->condition('bundle', $field_definition->getBundle()) + ->execute(); + } } /** @@ -1299,13 +1233,14 @@ public function onBundleRename($bundle, $bundle_new) { // @todo Use the unified store of deleted field definitions instead in // https://www.drupal.org/node/2282119 $field_definitions += entity_load_multiple_by_properties('field_instance_config', array('entity_type' => $this->entityTypeId, 'bundle' => $bundle, 'deleted' => TRUE, 'include_deleted' => TRUE)); + $table_mapping = $this->getTableMapping(); foreach ($field_definitions as $field_definition) { $storage_definition = $field_definition->getFieldStorageDefinition(); - if ($this->usesDedicatedTable($storage_definition)) { + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { $is_deleted = $this->storageDefinitionIsDeleted($storage_definition); - $table_name = static::_fieldTableName($storage_definition, $is_deleted); - $revision_name = static::_fieldRevisionTableName($storage_definition, $is_deleted); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted); $this->database->update($table_name) ->fields(array('bundle' => $bundle_new)) ->condition('bundle', $bundle) @@ -1326,13 +1261,14 @@ protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definit // bundle fields. $storage_definition = $field_definition->getFieldStorageDefinition(); $is_deleted = $this->storageDefinitionIsDeleted($storage_definition); - $table_name = static::_fieldTableName($storage_definition, $is_deleted); + $table_mapping = $this->getTableMapping(); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); // Get the entities which we want to purge first. $entity_query = $this->database->select($table_name, 't', array('fetch' => \PDO::FETCH_ASSOC)); $or = $entity_query->orConditionGroup(); foreach ($storage_definition->getColumns() as $column_name => $data) { - $or->isNotNull(static::_fieldColumnName($storage_definition, $column_name)); + $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name)); } $entity_query ->distinct(TRUE) @@ -1343,7 +1279,7 @@ protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definit // Create a map of field data table column names to field column names. $column_map = array(); foreach ($storage_definition->getColumns() as $column_name => $data) { - $column_map[static::_fieldColumnName($storage_definition, $column_name)] = $column_name; + $column_map[$table_mapping->getFieldColumnName($storage_definition, $column_name)] = $column_name; } $entities = array(); @@ -1383,8 +1319,9 @@ protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definit protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) { $storage_definition = $field_definition->getFieldStorageDefinition(); $is_deleted = $this->storageDefinitionIsDeleted($storage_definition); - $table_name = static::_fieldTableName($storage_definition, $is_deleted); - $revision_name = static::_fieldRevisionTableName($storage_definition, $is_deleted); + $table_mapping = $this->getTableMapping(); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); + $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted); $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id(); $this->database->delete($table_name) ->condition('revision_id', $revision_id) @@ -1398,10 +1335,7 @@ protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefiniti * {@inheritdoc} */ public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) { - $table_name = static::_fieldTableName($storage_definition, TRUE); - $revision_name = static::_fieldRevisionTableName($storage_definition, TRUE); - $this->database->schema()->dropTable($table_name); - $this->database->schema()->dropTable($revision_name); + $this->schemaHandler()->deleteFieldSchema($storage_definition); } /** @@ -1409,12 +1343,13 @@ public function finalizePurge(FieldStorageDefinitionInterface $storage_definitio */ public function countFieldData($storage_definition, $as_bool = FALSE) { $is_deleted = $this->storageDefinitionIsDeleted($storage_definition); - $table_name = static::_fieldTableName($storage_definition, $is_deleted); + $table_mapping = $this->getTableMapping(); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); $query = $this->database->select($table_name, 't'); $or = $query->orConditionGroup(); foreach ($storage_definition->getColumns() as $column_name => $data) { - $or->isNotNull(static::_fieldColumnName($storage_definition, $column_name)); + $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name)); } $query ->condition($or) @@ -1442,315 +1377,4 @@ protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $s return !array_key_exists($storage_definition->getName(), $this->entityManager->getFieldStorageDefinitions($this->entityTypeId)); } - /** - * Gets the SQL table schema. - * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field storage definition. - * @param array $schema - * The field schema array. Mandatory for upgrades, omit otherwise. - * @param bool $deleted - * (optional) Whether the schema of the table holding the values of a - * deleted field should be returned. - * - * @return array - * The same as a hook_schema() implementation for the data and the - * revision tables. - * - * @see hook_schema() - */ - public static function _fieldSqlSchema(FieldStorageDefinitionInterface $storage_definition, array $schema = NULL, $deleted = FALSE) { - $description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; - $description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; - - $entity_type_id = $storage_definition->getTargetEntityTypeId(); - $entity_manager = \Drupal::entityManager(); - $entity_type = $entity_manager->getDefinition($entity_type_id); - $definitions = $entity_manager->getBaseFieldDefinitions($entity_type_id); - - // Define the entity ID schema based on the field definitions. - $id_definition = $definitions[$entity_type->getKey('id')]; - if ($id_definition->getType() == 'integer') { - $id_schema = array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'The entity id this data is attached to', - ); - } - else { - $id_schema = array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'description' => 'The entity id this data is attached to', - ); - } - - // 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 = static::_fieldColumnName($storage_definition, $column_name); - $current['fields'][$real_name] = $attributes; - } - - // Add unique keys. - foreach ($schema['unique keys'] as $unique_key_name => $columns) { - $real_name = static::_fieldIndexName($storage_definition, $unique_key_name); - foreach ($columns as $column_name) { - $current['unique keys'][$real_name][] = static::_fieldColumnName($storage_definition, $column_name); - } - } - - // Add indexes. - foreach ($schema['indexes'] as $index_name => $columns) { - $real_name = static::_fieldIndexName($storage_definition, $index_name); - foreach ($columns as $column_name) { - // Indexes can be specified as either a column name or an array with - // column name and length. Allow for either case. - if (is_array($column_name)) { - $current['indexes'][$real_name][] = array( - static::_fieldColumnName($storage_definition, $column_name[0]), - $column_name[1], - ); - } - else { - $current['indexes'][$real_name][] = static::_fieldColumnName($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 = static::_fieldColumnName($storage_definition, $column_name); - $current['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced; - } - } - - // Construct the revision table. - $revision = $current; - $revision['description'] = $description_revision; - $revision['primary key'] = array('entity_id', 'revision_id', 'deleted', 'delta', 'langcode'); - $revision['fields']['revision_id']['not null'] = TRUE; - $revision['fields']['revision_id']['description'] = 'The entity revision id this data is attached to'; - - return array( - static::_fieldTableName($storage_definition) => $current, - static::_fieldRevisionTableName($storage_definition) => $revision, - ); - } - - /** - * Generates a table name for a field data table. - * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. Only - * call this function to write a query that \Drupal::entityQuery() does not - * support. Always call entity_load() before using the data found in the - * table. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field storage definition. - * @param bool $is_deleted - * (optional) Whether the table name holding the values of a deleted field - * should be returned. - * - * @return string - * A string containing the generated name for the database table. - */ - public static function _fieldTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) { - if ($is_deleted) { - // When a field is a deleted, the table is renamed to - // {field_deleted_data_FIELD_UUID}. To make sure we don't end up with - // table names longer than 64 characters, we hash the unique storage - // identifier and return the first 10 characters so we end up with a short - // unique ID. - return "field_deleted_data_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); - } - else { - return static::_generateFieldTableName($storage_definition, FALSE); - } - } - - /** - * Generates a table name for a field revision archive table. - * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. Only - * call this function to write a query that \Drupal::entityQuery() does not - * support. Always call entity_load() before using the data found in the - * table. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field storage definition. - * @param bool $is_deleted - * (optional) Whether the table name holding the values of a deleted field - * should be returned. - * - * @return string - * A string containing the generated name for the database table. - */ - public static function _fieldRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) { - if ($is_deleted) { - // When a field is a deleted, the table is renamed to - // {field_deleted_revision_FIELD_UUID}. To make sure we don't end up with - // table names longer than 64 characters, we hash the unique storage - // identifier and return the first 10 characters so we end up with a short - // unique ID. - return "field_deleted_revision_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); - } - else { - return static::_generateFieldTableName($storage_definition, TRUE); - } - } - - /** - * Generates a safe and unanbiguous field table name. - * - * The method accounts for a maximum table name length of 64 characters, and - * takes care of disambiguation. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field storage definition. - * @param bool $revision - * TRUE for revision table, FALSE otherwise. - * - * @return string - * The final table name. - */ - protected static function _generateFieldTableName(FieldStorageDefinitionInterface $storage_definition, $revision) { - $separator = $revision ? '_revision__' : '__'; - $table_name = $storage_definition->getTargetEntityTypeId() . $separator . $storage_definition->getName(); - // Limit the string to 48 characters, keeping a 16 characters margin for db - // prefixes. - if (strlen($table_name) > 48) { - // Use a shorter separator, a truncated entity_type, and a hash of the - // field UUID. - $separator = $revision ? '_r__' : '__'; - // Truncate to the same length for the current and revision tables. - $entity_type = substr($storage_definition->getTargetEntityTypeId(), 0, 34); - $field_hash = substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); - $table_name = $entity_type . $separator . $field_hash; - } - return $table_name; - } - - /** - * Generates an index name for a field data table. - * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field storage definition. - * @param string $index - * The name of the index. - * - * @return string - * A string containing a generated index name for a field data table that is - * unique among all other fields. - */ - public static function _fieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) { - return $storage_definition->getName() . '_' . $index; - } - - /** - * Generates a column name for a field data table. - * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. Only - * call this function to write a query that \Drupal::entityQuery() does not - * support. Always call entity_load() before using the data found in the - * table. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field storage definition. - * @param string $column - * The name of the column. - * - * @return string - * A string containing a generated column name for a field data table that is - * unique among all other fields. - */ - public static function _fieldColumnName(FieldStorageDefinitionInterface $storage_definition, $column) { - return in_array($column, FieldConfig::getReservedColumns()) ? $column : $storage_definition->getName() . '_' . $column; - } - } diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php index d5a6334..ae7c13e 100644 --- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php +++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php @@ -112,15 +112,19 @@ public function addField($field, $type, $langcode) { else { $field = FALSE; } + // If we managed to retrieve a configurable field, process it. if ($field instanceof FieldConfigInterface) { // Find the field column. $column = $field->getMainPropertyName(); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping(); + if ($key < $count) { $next = $specifiers[$key + 1]; // Is this a field column? $columns = $field->getColumns(); - if (isset($columns[$next]) || in_array($next, FieldConfig::getReservedColumns())) { + if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) { // Use it. $column = $next; // Do not process it again. @@ -142,7 +146,7 @@ public function addField($field, $type, $langcode) { } } $table = $this->ensureFieldTable($index_prefix, $field, $type, $langcode, $base_table, $entity_id_field, $field_id_field); - $sql_column = ContentEntityDatabaseStorage::_fieldColumnName($field, $column); + $sql_column = $table_mapping->getFieldColumnName($field, $column); } // This is an entity base field (non-configurable field). else { @@ -220,11 +224,13 @@ protected function ensureEntityTable($index_prefix, $property, $type, $langcode, protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field) { $field_name = $field->getName(); if (!isset($this->fieldTables[$index_prefix . $field_name])) { - $table = $this->sqlQuery->getMetaData('age') == EntityStorageInterface::FIELD_LOAD_CURRENT ? ContentEntityDatabaseStorage::_fieldTableName($field) : ContentEntityDatabaseStorage::_fieldRevisionTableName($field); + $entity_type_id = $this->sqlQuery->getMetaData('entity_type'); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping(); + $table = $this->sqlQuery->getMetaData('age') == EntityStorageInterface::FIELD_LOAD_CURRENT ? $table_mapping->getDedicatedDataTableName($field) : $table_mapping->getDedicatedRevisionTableName($field); if ($field->getCardinality() != 1) { $this->sqlQuery->addMetaData('simple_query', FALSE); } - $entity_type = $this->sqlQuery->getMetaData('entity_type'); $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode); } return $this->fieldTables[$index_prefix . $field_name]; diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php index 2294794..272a3ab 100644 --- a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php @@ -7,9 +7,12 @@ namespace Drupal\Core\Entity\Schema; +use Drupal\Core\Database\Connection; use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; +use Drupal\Core\Field\FieldStorageDefinitionInterface; /** * Defines a schema handler that supports revisionable, translatable entities. @@ -45,6 +48,13 @@ class ContentEntitySchemaHandler implements EntitySchemaHandlerInterface { protected $schema; /** + * The database connection to be used. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** * Constructs a ContentEntitySchemaHandler. * * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager @@ -53,11 +63,343 @@ class ContentEntitySchemaHandler implements EntitySchemaHandlerInterface { * The entity type. * @param \Drupal\Core\Entity\ContentEntityDatabaseStorage $storage * The storage of the entity type. This must be an SQL-based storage. + * @param \Drupal\Core\Database\Connection $database + * The database connection to be used. */ - public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorage $storage) { + public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorage $storage, Connection $database) { $this->entityType = $entity_type; $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id()); $this->storage = $storage; + $this->database = $database; + } + + /** + * 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 performSchemaOperation($operation, FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original = NULL) { + $table_mapping = $this->storage->getTableMapping(); + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + $this->{$operation . 'DedicatedTableSchema'}($storage_definition, $original); + } + elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) { + $this->{$operation . 'SharedTableSchema'}($storage_definition, $original); + } + } + + /** + * {@inheritdoc} + */ + public function createFieldSchema(FieldStorageDefinitionInterface $storage_definition) { + $this->performSchemaOperation('create', $storage_definition); + } + + /** + * Creates the schema for a field stored in a dedicated table. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The storage definition of the field being created. + */ + protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) { + $schema = $this->getDedicatedTableSchema($storage_definition); + foreach ($schema as $name => $table) { + $this->database->schema()->createTable($name, $table); + } + } + + /** + * Creates the schema for a field stored in a shared table. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The storage definition of the field being created. + */ + protected function createSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) { + $created_field_name = $storage_definition->getName(); + $table_mapping = $this->storage->getTableMapping(); + $column_names = $table_mapping->getColumnNames($created_field_name); + $schema = $this->getSharedTableFieldSchema($storage_definition, $column_names); + $keys = array_diff_key($schema, array('fields' => FALSE)); + + // Iterate over the mapped table to find the ones that will host the created + // field schema. + foreach ($table_mapping->getTableNames() as $table_name) { + foreach ($table_mapping->getFieldNames($table_name) as $field_name) { + if ($field_name == $created_field_name) { + foreach ($schema['fields'] as $column_name => $specifier) { + $this->database->schema()->addField($table_name, $column_name, $specifier, $keys); + } + // After creating the field schema skip to the next table. + break; + } + } + } + } + + /** + * {@inheritdoc} + */ + public function markFieldSchemaAsDeleted(FieldStorageDefinitionInterface $storage_definition) { + $table_mapping = $this->storage->getTableMapping(); + // TODO Do we need this also for shared table storage? + 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); + $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition); + $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); + } + } + + /** + * {@inheritdoc} + */ + public function deleteFieldSchema(FieldStorageDefinitionInterface $storage_definition) { + $this->performSchemaOperation('delete', $storage_definition); + } + + /** + * 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) { + $table_mapping = $this->storage->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); + } + + /** + * 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(); + $column_names = $table_mapping->getColumnNames($deleted_field_name); + $schema = $this->getSharedTableFieldSchema($storage_definition, $column_names); + $schema_handler = $this->database->schema(); + + // Iterate over the mapped table to find the ones that host the deleted + // field schema. + foreach ($table_mapping->getTableNames() as $table_name) { + foreach ($table_mapping->getFieldNames($table_name) as $field_name) { + if ($field_name == $deleted_field_name) { + // Drop indexes and unique keys first. + if (!empty($schema['indexes'])) { + foreach ($schema['indexes'] as $name => $specifier) { + $schema_handler->dropIndex($table_name, $name); + } + } + if (!empty($schema['unique keys'])) { + foreach ($schema['unique keys'] as $name => $specifier) { + $schema_handler->dropUniqueKey($table_name, $name); + } + } + // Drop columns. + foreach ($column_names as $column_name) { + $schema_handler->dropField($table_name, $column_name); + } + // After deleting the field schema skip to the next table. + break; + } + } + } + } + + /** + * {@inheritdoc} + */ + public function updateFieldSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + $this->performSchemaOperation('update', $storage_definition, $original); + } + + /** + * 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. + */ + protected function updateDedicatedTableSchema(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->getDedicatedTableSchema($original); + foreach ($original_schema as $name => $table) { + $this->database->schema()->dropTable($name, $table); + } + $schema = $this->getDedicatedTableSchema($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->getDedicatedTableSchema($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->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); + } + } + } + } + + /** + * 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. + */ + protected function updateSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + if (!$this->storage->countFieldData($storage_definition, 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 { + $this->deleteSharedTableSchema($original); + $this->createSharedTableSchema($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."); + } + + $schema = array(); + $original_schema = array(); + $updated_field_name = $storage_definition->getName(); + $table_mapping = $this->storage->getTableMapping(); + $column_names = $table_mapping->getColumnNames($updated_field_name); + $original_schema = $this->getSharedTableFieldSchema($original, $column_names); + $schema = $this->getSharedTableFieldSchema($storage_definition, $column_names); + $schema_handler = $this->database->schema(); + + // Iterate over the mapped table to find the ones that host the deleted + // field schema. + foreach ($table_mapping->getTableNames() as $table_name) { + foreach ($table_mapping->getFieldNames($table_name) as $field_name) { + if ($field_name == $updated_field_name) { + // Drop original indexes and unique keys. + if (!empty($original_schema['indexes'])) { + foreach ($original_schema['indexes'] as $name => $specifier) { + $schema_handler->dropIndex($table_name, $name); + } + } + if (!empty($original_schema['unique keys'])) { + foreach ($original_schema['unique keys'] as $name => $specifier) { + $schema_handler->dropUniqueKey($table_name, $name); + } + } + // Create new indexes and unique keys. + if (!empty($schema['indexes'])) { + foreach ($schema['indexes'] as $name => $specifier) { + $schema_handler->addIndex($table_name, $name, $specifier); + } + } + if (!empty($schema['unique keys'])) { + foreach ($schema['unique keys'] as $name => $specifier) { + $schema_handler->addUniqueKey($table_name, $name, $specifier); + } + } + // After deleting the field schema skip to the next table. + break; + } + } + } + } } /** @@ -82,10 +424,16 @@ public function getSchema() { $table_mapping = $this->storage->getTableMapping(); foreach ($table_mapping->getTableNames() as $table_name) { - // Add the schema from field definitions. + 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); + // Add the schema for base field definitions. + if ($table_mapping->allowsSharedTableStorage($this->fieldStorageDefinitions[$field_name])) { + $column_names = $table_mapping->getColumnNames($field_name); + $storage_definition = $this->fieldStorageDefinitions[$field_name]; + $schema[$table_name] = array_merge_recursive($schema[$table_name], $this->getSharedTableFieldSchema($storage_definition, $column_names)); + } } // Add the schema for extra fields. @@ -132,16 +480,22 @@ protected function getTables() { /** * Returns the schema for a single field definition. * - * @param array $schema - * The table schema to add the field schema to, passed by reference. - * @param string $field_name - * The name of the field. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The storage definition of the field whose schema has to be returned. * @param string[] $column_mapping * A mapping of field column names to database column names. */ - protected function addFieldSchema(array &$schema, $field_name, array $column_mapping) { - $field_schema = $this->fieldStorageDefinitions[$field_name]->getSchema(); - $field_description = $this->fieldStorageDefinitions[$field_name]->getDescription(); + protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, array $column_mapping) { + $schema = array(); + $field_schema = $storage_definition->getSchema(); + + // Check that the schema does not include forbidden column names. + if (array_intersect(array_keys($field_schema['columns']), $this->storage->getTableMapping()->getReservedColumns())) { + throw new FieldException('Illegal field type columns.'); + } + + $field_name = $storage_definition->getName(); + $field_description = $storage_definition->getDescription(); foreach ($column_mapping as $field_column_name => $schema_field_name) { $column_schema = $field_schema['columns'][$field_column_name]; @@ -166,19 +520,18 @@ protected function addFieldSchema(array &$schema, $field_name, array $column_map } if (!empty($field_schema['indexes'])) { - $indexes = $this->getFieldIndexes($field_name, $field_schema, $column_mapping); - $schema['indexes'] = array_merge($schema['indexes'], $indexes); + $schema['indexes'] = $this->getFieldIndexes($field_name, $field_schema, $column_mapping); } if (!empty($field_schema['unique keys'])) { - $unique_keys = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping); - $schema['unique keys'] = array_merge($schema['unique keys'], $unique_keys); + $schema['unique keys'] = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping); } if (!empty($field_schema['foreign keys'])) { - $foreign_keys = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping); - $schema['foreign keys'] = array_merge($schema['foreign keys'], $foreign_keys); + $schema['foreign keys'] = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping); } + + return $schema; } /** @@ -309,6 +662,171 @@ protected function addDefaultLangcodeSchema(&$schema) { ); } + + /** + * Returns the SQL schema for a dedicated table. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * + * FIXME Do we still need these two? The deleted parameter is not used. + * + * @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() + */ + protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, array $schema = NULL, $deleted = FALSE) { + $description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; + $description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; + + $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, default to integer if there is no revision + // ID. + // @todo Revisit this code: the revision id should match the entity id type + // if revisions are not supported. + $revision_id_definition = $this->entityType->isRevisionable() ? $this->fieldStorageDefinitions[$this->entityType->getKey('revision')] : NULL; + if (!$revision_id_definition || $revision_id_definition->getType() == 'integer') { + $revision_id_schema = array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned', + ); + } + else { + $revision_id_schema = array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => FALSE, + 'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned', + ); + } + + $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'), + ), + ); + + if (!$schema) { + $schema = $storage_definition->getSchema(); + } + + // Check that the schema does not include forbidden column names. + $table_mapping = $this->storage->getTableMapping(); + if (array_intersect(array_keys($schema['columns']), $table_mapping->getReservedColumns())) { + throw new FieldException(format_string('Illegal field type @field_type on @field_name.', array('@field_type' => $this->type, '@field_name' => $this->name))); + } + + // 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; + } + } + + // Construct the revision table. + $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'; + + return array( + $table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema, + $table_mapping->getDedicatedRevisionTableName($storage_definition) => $revision_schema, + ); + } + /** * Initializes common information for a base table. * @@ -341,6 +859,26 @@ protected function initializeBaseTable() { } /** + * 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. + */ + protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) { + return $storage_definition->getName() . '_' . $index; + } + + /** * Initializes common information for a revision table. * * @return array diff --git a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php index a38b82c..c7fe82d 100644 --- a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php +++ b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php @@ -7,8 +7,48 @@ namespace Drupal\Core\Entity\Schema; +use Drupal\Core\Field\FieldStorageDefinitionInterface; + /** * Defines an interface for handling the storage schema of entities. */ interface EntitySchemaHandlerInterface extends EntitySchemaProviderInterface { + + /** + * Creates the storage schema for the given field. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The storage definition of the field being created. + */ + public function createFieldSchema(FieldStorageDefinitionInterface $storage_definition); + + /** + * Marks the storage schema for the given field as deleted. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The storage definition of the field being deleted. + */ + public function markFieldSchemaAsDeleted(FieldStorageDefinitionInterface $storage_definition); + + /** + * Deletes the storage schema for the given field. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The storage definition of the field being deleted. + */ + public function deleteFieldSchema(FieldStorageDefinitionInterface $storage_definition); + + /** + * Updates the storage schema for the given field. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The storage definition of the field being updated. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original + * The original storage definition; i.e., the definition before the update. + * + * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException + * Thrown when the update to the field is forbidden. + */ + public function updateFieldSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original); + } diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php index e872d57..b0a5522 100644 --- a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php +++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php @@ -7,10 +7,12 @@ namespace Drupal\Core\Entity\Sql; +use Drupal\Core\Field\FieldStorageDefinitionInterface; + /** * Defines a default table mapping class. */ -class DefaultTableMapping implements TableMappingInterface { +class DefaultTableMapping implements DefaultTableMappingInterface { /** * A list of field storage definitions that are available for this mapping. @@ -20,6 +22,13 @@ class DefaultTableMapping implements TableMappingInterface { protected $fieldStorageDefinitions = array(); /** + * A list of base field definitions that are available for this mapping. + * + * @var \Drupal\Core\Field\FieldDefinitionInterface[] + */ + protected $baseFieldDefinitions = array(); + + /** * A list of field names per table. * * This corresponds to the return value of @@ -76,9 +85,13 @@ class DefaultTableMapping implements TableMappingInterface { * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions * A list of field storage definitions that should be available for the * field columns of this table mapping. + * @param \Drupal\Core\Field\FieldDefinitionInterface[] $base_field_definitions + * A list of base field definitions that should be available for the field + * columns of this table mapping. */ - public function __construct(array $storage_definitions) { + public function __construct(array $storage_definitions, array $base_field_definitions) { $this->fieldStorageDefinitions = $storage_definitions; + $this->baseFieldDefinitions = $base_field_definitions; } /** @@ -99,7 +112,14 @@ 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])) { + $this->allColumns[$table_name] = array_merge($this->getExtraColumns($table_name), $this->allColumns[$table_name]); + } + else { + $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], $this->getExtraColumns($table_name)); + } } return $this->allColumns[$table_name]; } @@ -177,4 +197,105 @@ public function setExtraColumns($table_name, array $column_names) { return $this; } + /** + * {@inheritdoc} + */ + function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition) { + return !$storage_definition->hasCustomStorage() && isset($this->baseFieldDefinitions[$storage_definition->getName()]) && !$storage_definition->isMultiple(); + } + + /** + * {@inheritdoc} + */ + function requiresDedicatedTableStorage(FieldStorageDefinitionInterface $storage_definition) { + return !$storage_definition->hasCustomStorage() && (!isset($this->baseFieldDefinitions[$storage_definition->getName()]) || $storage_definition->isMultiple()); + } + + /** + * {@inheritdoc} + */ + function getDedicatedTableNames() { + // TODO + return array(); + } + + /** + * {@inheritdoc} + */ + public function getReservedColumns() { + return array('deleted'); + } + + /** + * {@inheritdoc} + */ + public function getDedicatedDataTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) { + if ($is_deleted) { + // When a field is a deleted, the table is renamed to + // {field_deleted_data_FIELD_UUID}. To make sure we don't end up with + // table names longer than 64 characters, we hash the unique storage + // identifier and return the first 10 characters so we end up with a short + // unique ID. + return "field_deleted_data_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); + } + else { + return $this->generateFieldTableName($storage_definition, FALSE); + } + } + + /** + * {@inheritdoc} + */ + public function getDedicatedRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) { + if ($is_deleted) { + // When a field is a deleted, the table is renamed to + // {field_deleted_revision_FIELD_UUID}. To make sure we don't end up with + // table names longer than 64 characters, we hash the unique storage + // identifier and return the first 10 characters so we end up with a short + // unique ID. + return "field_deleted_revision_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); + } + else { + return $this->generateFieldTableName($storage_definition, TRUE); + } + } + + /** + * Generates a safe and unambiguous field table name. + * + * The method accounts for a maximum table name length of 64 characters, and + * takes care of disambiguation. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + * @param bool $revision + * TRUE for revision table, FALSE otherwise. + * + * @return string + * The final table name. + */ + protected function generateFieldTableName(FieldStorageDefinitionInterface $storage_definition, $revision) { + $separator = $revision ? '_revision__' : '__'; + $table_name = $storage_definition->getTargetEntityTypeId() . $separator . $storage_definition->getName(); + // Limit the string to 48 characters, keeping a 16 characters margin for db + // prefixes. + if (strlen($table_name) > 48) { + // Use a shorter separator, a truncated entity_type, and a hash of the + // field UUID. + $separator = $revision ? '_r__' : '__'; + // Truncate to the same length for the current and revision tables. + $entity_type = substr($storage_definition->getTargetEntityTypeId(), 0, 34); + $field_hash = substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); + $table_name = $entity_type . $separator . $field_hash; + } + return $table_name; + } + + /** + * {@inheritdoc} + */ + public function getFieldColumnName(FieldStorageDefinitionInterface $storage_definition, $column) { + return in_array($column, $this->getReservedColumns()) ? $column : $storage_definition->getName() . '_' . $column; + } + } diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMappingInterface.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMappingInterface.php new file mode 100644 index 0000000..a9e0ebf --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMappingInterface.php @@ -0,0 +1,121 @@ + array(), ); - // Check that the schema does not include forbidden column names. - if (array_intersect(array_keys($schema['columns']), static::getReservedColumns())) { - throw new FieldException('Illegal field type columns.'); - } - // Merge custom indexes with those specified by the field type. Custom // indexes prevail. $schema['indexes'] = $this->indexes + $schema['indexes']; @@ -583,15 +578,6 @@ public function getColumns() { } /** - * A list of columns that can not be used as field type columns. - * - * @return array - */ - public static function getReservedColumns() { - return array('deleted'); - } - - /** * {@inheritdoc} */ public function hasCustomStorage() { diff --git a/core/modules/contact/src/Tests/Views/ContactFieldsTest.php b/core/modules/contact/src/Tests/Views/ContactFieldsTest.php index 4308ca7..5e456a4 100644 --- a/core/modules/contact/src/Tests/Views/ContactFieldsTest.php +++ b/core/modules/contact/src/Tests/Views/ContactFieldsTest.php @@ -66,7 +66,7 @@ protected function setUp() { public function testViewsData() { // Test that the field is not exposed to views, since contact_message // entities have no storage. - $table_name = ContentEntityDatabaseStorage::_fieldTableName($this->field); + $table_name = 'contact_message__' . $this->field->getName(); $data = $this->container->get('views.views_data')->get($table_name); $this->assertFalse($data, 'The field is not exposed to Views.'); } diff --git a/core/modules/field/field.views.inc b/core/modules/field/field.views.inc index f74e97c..dd52cf3 100644 --- a/core/modules/field/field.views.inc +++ b/core/modules/field/field.views.inc @@ -22,7 +22,7 @@ function field_views_data() { $module_handler = \Drupal::moduleHandler(); foreach (\Drupal::entityManager()->getStorage('field_config')->loadMultiple() as $field) { - if (_field_views_is_sql_entity_type($field)) { + if (_field_views_get_entity_type_storage($field)) { $result = (array) $module_handler->invoke($field->module, 'field_views_data', array($field)); if (empty($result)) { $result = field_views_field_default_views_data($field); @@ -48,7 +48,7 @@ function field_views_data() { */ function field_views_data_alter(&$data) { foreach (\Drupal::entityManager()->getStorage('field_config')->loadMultiple() as $field) { - if (_field_views_is_sql_entity_type($field)) { + if (_field_views_get_entity_type_storage($field)) { $function = $field->module . '_field_views_data_views_data_alter'; if (function_exists($function)) { $function($data, $field); @@ -63,12 +63,17 @@ function field_views_data_alter(&$data) { * @param \Drupal\field\FieldConfigInterface $field * The field definition. * - * @return bool - * True if the entity type uses ContentEntityDatabaseStorage. + * @return \Drupal\Core\Entity\ContentEntityDatabaseStorage + * Returns the entity type storage if supported. */ -function _field_views_is_sql_entity_type(FieldConfigInterface $field) { +function _field_views_get_entity_type_storage(FieldConfigInterface $field) { + $result = FALSE; $entity_manager = \Drupal::entityManager(); - return $entity_manager->hasDefinition($field->entity_type) && $entity_manager->getStorage($field->entity_type) instanceof ContentEntityDatabaseStorage; + if ($entity_manager->hasDefinition($field->entity_type)) { + $storage = $entity_manager->getStorage($field->entity_type); + $result = $storage instanceof ContentEntityDatabaseStorage ? $storage : FALSE; + } + return $result; } /** @@ -120,6 +125,11 @@ function field_views_field_default_views_data(FieldConfigInterface $field) { if (!$field->getBundles()) { return $data; } + // Check whether the entity type storage is supported. + $storage = _field_views_get_entity_type_storage($field); + if (!$storage) { + return $data; + } $field_name = $field->getName(); $field_columns = $field->getColumns(); @@ -139,15 +149,18 @@ function field_views_field_default_views_data(FieldConfigInterface $field) { } // Description of the field tables. + // @todo Generalize this code to make it work with any table layout. See + // https://drupal.org/node/2079019. + $table_mapping = $storage->getTableMapping(); $field_tables = array( EntityStorageInterface::FIELD_LOAD_CURRENT => array( - 'table' => ContentEntityDatabaseStorage::_fieldTableName($field), + 'table' => $table_mapping->getDedicatedDataTableName($field), 'alias' => "{$entity_type_id}__{$field_name}", ), ); if ($supports_revisions) { $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION] = array( - 'table' => ContentEntityDatabaseStorage::_fieldRevisionTableName($field), + 'table' => $table_mapping->getDedicatedRevisionTableName($field), 'alias' => "{$entity_type_id}_revision__{$field_name}", ); } @@ -178,7 +191,7 @@ function field_views_field_default_views_data(FieldConfigInterface $field) { // Build the list of additional fields to add to queries. $add_fields = array('delta', 'langcode', 'bundle'); foreach (array_keys($field_columns) as $column) { - $add_fields[] = ContentEntityDatabaseStorage::_fieldColumnName($field, $column); + $add_fields[] = $table_mapping->getFieldColumnName($field, $column); } // Determine the label to use for the field. We don't have a label available // at the field level, so we just go through all instances and take the one @@ -302,11 +315,10 @@ function field_views_field_default_views_data(FieldConfigInterface $field) { else { $group = t('@group (historical data)', array('@group' => $group_name)); } - $column_real_name = ContentEntityDatabaseStorage::_fieldColumnName($field, $column); + $column_real_name = $table_mapping->getFieldColumnName($field, $column); // Load all the fields from the table by default. - $field_sql_schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field); - $additional_fields = array_keys($field_sql_schema[$table]['fields']); + $additional_fields = $table_mapping->getAllColumns($table); $data[$table_alias][$column_real_name] = array( 'group' => $group, diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php index af025e3..7f80220 100644 --- a/core/modules/field/src/Entity/FieldConfig.php +++ b/core/modules/field/src/Entity/FieldConfig.php @@ -432,11 +432,6 @@ public function getSchema() { 'foreign keys' => array(), ); - // Check that the schema does not include forbidden column names. - if (array_intersect(array_keys($schema['columns']), static::getReservedColumns())) { - throw new FieldException(String::format('Illegal field type @field_type on @field_name.', array('@field_type' => $this->type, '@field_name' => $this->name))); - } - // Merge custom indexes with those specified by the field type. Custom // indexes prevail. $schema['indexes'] = $this->indexes + $schema['indexes']; @@ -616,15 +611,6 @@ public function isQueryable() { } /** - * A list of columns that can not be used as field type columns. - * - * @return array - */ - public static function getReservedColumns() { - return array('deleted'); - } - - /** * Determines whether a field has any data. * * @return bool diff --git a/core/modules/field/src/Plugin/views/field/Field.php b/core/modules/field/src/Plugin/views/field/Field.php index c678554..05c8342 100644 --- a/core/modules/field/src/Plugin/views/field/Field.php +++ b/core/modules/field/src/Plugin/views/field/Field.php @@ -349,9 +349,12 @@ public function clickSort($order) { } $this->ensureMyTable(); - $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->definition['entity_type']); + $entity_type_id = $this->definition['entity_type']; + $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); $field = $field_storage_definitions[$this->definition['field_name']]; - $column = ContentEntityDatabaseStorage::_fieldColumnName($field, $this->options['click_sort_column']); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping(); + $column = $table_mapping->getFieldColumnName($field, $this->options['click_sort_column']); if (!isset($this->aliases[$column])) { // Column is not in query; add a sort on it (without adding the column). $this->aliases[$column] = $this->tableAlias . '.' . $column; diff --git a/core/modules/field/src/Tests/BulkDeleteTest.php b/core/modules/field/src/Tests/BulkDeleteTest.php index 8e96eab..41985eb 100644 --- a/core/modules/field/src/Tests/BulkDeleteTest.php +++ b/core/modules/field/src/Tests/BulkDeleteTest.php @@ -186,11 +186,13 @@ function testDeleteFieldInstance() { $this->assertEqual($instance->bundle, $bundle, 'The deleted instance is for the correct bundle'); // Check that the actual stored content did not change during delete. - $schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field); - $table = ContentEntityDatabaseStorage::_fieldTableName($field); - $column = ContentEntityDatabaseStorage::_fieldColumnName($field, 'value'); + $storage = \Drupal::entityManager()->getStorage($this->entity_type); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $storage->getTableMapping(); + $table = $table_mapping->getDedicatedDataTableName($field); + $column = $table_mapping->getFieldColumnName($field, 'value'); $result = db_select($table, 't') - ->fields('t', array_keys($schema[$table]['fields'])) + ->fields('t') ->execute(); foreach ($result as $row) { $this->assertEqual($this->entities[$row->entity_id]->{$field->name}->value, $row->$column); diff --git a/core/modules/field/src/Tests/FieldDataCountTest.php b/core/modules/field/src/Tests/FieldDataCountTest.php index 3275b47..78ce402 100644 --- a/core/modules/field/src/Tests/FieldDataCountTest.php +++ b/core/modules/field/src/Tests/FieldDataCountTest.php @@ -85,7 +85,8 @@ public function testEntityCountAndHasData() { $storage = \Drupal::entityManager()->getStorage('entity_test'); if ($storage instanceof ContentEntityDatabaseStorage) { // Count the actual number of rows in the field table. - $field_table_name = $storage->_fieldTableName($field); + $table_mapping = $storage->getTableMapping(); + $field_table_name = $table_mapping->getDedicatedDataTableName($field); $result = db_select($field_table_name, 't') ->fields('t') ->countQuery() diff --git a/core/modules/field/src/Tests/Views/ApiDataTest.php b/core/modules/field/src/Tests/Views/ApiDataTest.php index be0d496..3f05fec 100644 --- a/core/modules/field/src/Tests/Views/ApiDataTest.php +++ b/core/modules/field/src/Tests/Views/ApiDataTest.php @@ -62,8 +62,10 @@ function testViewsData() { // Check the table and the joins of the first field. // Attached to node only. $field = $this->fields[0]; - $current_table = ContentEntityDatabaseStorage::_fieldTableName($field); - $revision_table = ContentEntityDatabaseStorage::_fieldRevisionTableName($field); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = \Drupal::entityManager()->getStorage('node')->getTableMapping(); + $current_table = $table_mapping->getDedicatedDataTableName($field); + $revision_table = $table_mapping->getDedicatedRevisionTableName($field); $data[$current_table] = $views_data->get($current_table); $data[$revision_table] = $views_data->get($revision_table); diff --git a/core/modules/file/file.views.inc b/core/modules/file/file.views.inc index e1029d5..959e141 100644 --- a/core/modules/file/file.views.inc +++ b/core/modules/file/file.views.inc @@ -520,9 +520,12 @@ function file_field_views_data(FieldConfigInterface $field) { */ function file_field_views_data_views_data_alter(array &$data, FieldConfigInterface $field) { $entity_type_id = $field->entity_type; - $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); + $entity_manager = \Drupal::entityManager(); + $entity_type = $entity_manager->getDefinition($entity_type_id); $field_name = $field->getName(); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); list($label) = field_views_field_label($entity_type_id, $field_name); @@ -532,7 +535,7 @@ function file_field_views_data_views_data_alter(array &$data, FieldConfigInterfa 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field), + 'field table' => $table_mapping->getDedicatedDataTableName($field), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), diff --git a/core/modules/image/image.views.inc b/core/modules/image/image.views.inc index fe2d59a..d9ffdbb 100644 --- a/core/modules/image/image.views.inc +++ b/core/modules/image/image.views.inc @@ -39,8 +39,11 @@ function image_field_views_data(FieldConfigInterface $field) { function image_field_views_data_views_data_alter(array &$data, FieldConfigInterface $field) { $entity_type_id = $field->entity_type; $field_name = $field->getName(); - $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); + $entity_manager = \Drupal::entityManager(); + $entity_type = $entity_manager->getDefinition($entity_type_id); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); list($label) = field_views_field_label($entity_type_id, $field_name); @@ -50,7 +53,7 @@ function image_field_views_data_views_data_alter(array &$data, FieldConfigInterf 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field), + 'field table' => $table_mapping->getDedicatedDataTableName($field), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), diff --git a/core/modules/options/options.module b/core/modules/options/options.module index d9a1b24..32ff90f 100644 --- a/core/modules/options/options.module +++ b/core/modules/options/options.module @@ -6,11 +6,10 @@ */ use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Drupal\field\FieldConfigInterface; -use Drupal\field\FieldConfigUpdateForbiddenException; /** * Implements hook_help(). diff --git a/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php b/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php index 0023397..0c7b5bd 100644 --- a/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php +++ b/core/modules/system/src/Tests/Entity/EntityBundleFieldTest.php @@ -7,6 +7,8 @@ namespace Drupal\system\Tests\Entity; +use Drupal\Core\Entity\Sql\DefaultTableMappingInterface; + /** * Tests adding a custom bundle field. */ @@ -65,7 +67,9 @@ public function testCustomBundleFieldCreateDelete() { $this->assertNotNull($definition, 'Field definition found.'); // Make sure the table has been created. - $table = $this->entityManager->getStorage('entity_test')->_fieldTableName($definition); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $this->entityManager->getStorage('entity_test')->getTableMapping(); + $table = $table_mapping->getDedicatedDataTableName($definition->getFieldStorageDefinition()); $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'); @@ -103,7 +107,9 @@ public function testCustomBundleFieldUsage() { $this->assertEqual($entity->custom_field->value, 'cozy', 'Entity was updated correctly.'); $entity->delete(); - $table = $storage->_fieldTableName($entity->getFieldDefinition('custom_field')); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $storage->getTableMapping(); + $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_field')); $result = $this->database->select($table, 'f') ->fields('f') ->condition('f.entity_id', $entity->id()) @@ -116,7 +122,7 @@ public function testCustomBundleFieldUsage() { $entity->save(); entity_test_delete_bundle('custom'); - $table = $storage->_fieldTableName($entity->getFieldDefinition('custom_field')); + $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_field')); $result = $this->database->select($table, 'f') ->condition('f.entity_id', $entity->id()) ->condition('deleted', 1) diff --git a/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php b/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php index 28cfed2..c0255c8 100644 --- a/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php +++ b/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php @@ -49,12 +49,26 @@ class FieldSqlStorageTest extends EntityUnitTestBase { protected $instance; /** + * Name of the data table of the field. + * + * @var string + */ + protected $table; + + /** * Name of the revision table of the field. * * @var string */ protected $revision_table; + /** + * The table mapping for the tested entity type. + * + * @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping + */ + protected $table_mapping; + public static function getInfo() { return array( 'name' => 'Field SQL storage tests', @@ -84,8 +98,11 @@ function setUp() { )); $this->instance->save(); - $this->table = ContentEntityDatabaseStorage::_fieldTableName($this->field); - $this->revision_table = ContentEntityDatabaseStorage::_fieldRevisionTableName($this->field); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = \Drupal::entityManager()->getStorage($entity_type)->getTableMapping(); + $this->table_mapping = $table_mapping; + $this->table = $table_mapping->getDedicatedDataTableName($this->field); + $this->revision_table = $table_mapping->getDedicatedRevisionTableName($this->field); } /** @@ -95,7 +112,7 @@ function testFieldLoad() { $entity_type = $bundle = 'entity_test_rev'; $storage = $this->container->get('entity.manager')->getStorage($entity_type); - $columns = array('bundle', 'deleted', 'entity_id', 'revision_id', 'delta', 'langcode', ContentEntityDatabaseStorage::_fieldColumnName($this->field, 'value')); + $columns = array('bundle', 'deleted', 'entity_id', 'revision_id', 'delta', 'langcode', $this->table_mapping->getFieldColumnName($this->field, 'value')); // Create an entity with four revisions. $revision_ids = array(); @@ -351,7 +368,11 @@ function testFieldUpdateFailure() { } // Ensure that the field tables are still there. - foreach (ContentEntityDatabaseStorage::_fieldSqlSchema($prior_field) as $table_name => $table_info) { + $tables = array( + $this->table_mapping->getDedicatedDataTableName($prior_field), + $this->table_mapping->getDedicatedRevisionTableName($prior_field), + ); + foreach ($tables as $table_name) { $this->assertTrue(db_table_exists($table_name), t('Table %table exists.', array('%table' => $table_name))); } } @@ -374,7 +395,7 @@ function testFieldUpdateIndexesWithData() { 'bundle' => $entity_type, )); $instance->save(); - $tables = array(ContentEntityDatabaseStorage::_fieldTableName($field), ContentEntityDatabaseStorage::_fieldRevisionTableName($field)); + $tables = array($this->table_mapping->getDedicatedDataTableName($field), $this->table_mapping->getDedicatedRevisionTableName($field)); // Verify the indexes we will create do not exist yet. foreach ($tables as $table) { @@ -445,14 +466,15 @@ function testFieldSqlStorageForeignKeys() { $this->assertEqual($schema['foreign keys'][$foreign_key_name]['table'], $foreign_key_name, 'Foreign key table name modified after update'); $this->assertEqual($schema['foreign keys'][$foreign_key_name]['columns'][$foreign_key_name], 'id', 'Foreign key column name modified after update'); - // Verify the SQL schema. - $schemas = ContentEntityDatabaseStorage::_fieldSqlSchema($field); - $schema = $schemas[ContentEntityDatabaseStorage::_fieldTableName($field)]; - $this->assertEqual(count($schema['foreign keys']), 1, 'There is 1 foreign key in the schema'); - $foreign_key = reset($schema['foreign keys']); - $foreign_key_column = ContentEntityDatabaseStorage::_fieldColumnName($field, $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'); + +// // Verify the SQL schema. TODO Move this to a unit test. +// $schemas = ContentEntityDatabaseStorage::_fieldSqlSchema($field); +// $schema = $schemas[$this->table_mapping->getDedicatedDataTableName($field)]; +// $this->assertEqual(count($schema['foreign keys']), 1, 'There is 1 foreign key in the schema'); +// $foreign_key = reset($schema['foreign keys']); +// $foreign_key_column = ContentEntityDatabaseStorage::_fieldColumnName($field, $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'); } /** @@ -500,9 +522,9 @@ public function testTableNames() { 'type' => 'test_field', )); $expected = 'short_entity_type__short_field_name'; - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field), $expected); $expected = 'short_entity_type_revision__short_field_name'; - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field), $expected); // Short entity type, long field name $entity_type = 'short_entity_type'; @@ -513,9 +535,9 @@ public function testTableNames() { 'type' => 'test_field', )); $expected = 'short_entity_type__' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field), $expected); $expected = 'short_entity_type_r__' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field), $expected); // Long entity type, short field name $entity_type = 'long_entity_type_abcdefghijklmnopqrstuvwxyz'; @@ -526,9 +548,9 @@ public function testTableNames() { 'type' => 'test_field', )); $expected = 'long_entity_type_abcdefghijklmnopq__' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field), $expected); $expected = 'long_entity_type_abcdefghijklmnopq_r__' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field), $expected); // Long entity type and field name. $entity_type = 'long_entity_type_abcdefghijklmnopqrstuvwxyz'; @@ -539,17 +561,17 @@ public function testTableNames() { 'type' => 'test_field', )); $expected = 'long_entity_type_abcdefghijklmnopq__' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field), $expected); $expected = 'long_entity_type_abcdefghijklmnopq_r__' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field), $expected); // Try creating a second field and check there are no clashes. $field2 = entity_create('field_config', array( 'entity_type' => $entity_type, 'name' => $field_name . '2', 'type' => 'test_field', )); - $this->assertNotEqual(ContentEntityDatabaseStorage::_fieldTableName($field), ContentEntityDatabaseStorage::_fieldTableName($field2)); - $this->assertNotEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field), ContentEntityDatabaseStorage::_fieldRevisionTableName($field2)); + $this->assertNotEqual($this->table_mapping->getDedicatedDataTableName($field), $this->table_mapping->getDedicatedDataTableName($field2)); + $this->assertNotEqual($this->table_mapping->getDedicatedRevisionTableName($field), $this->table_mapping->getDedicatedRevisionTableName($field2)); // Deleted field. $field = entity_create('field_config', array( @@ -559,9 +581,9 @@ public function testTableNames() { 'deleted' => TRUE, )); $expected = 'field_deleted_data_' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldTableName($field, TRUE), $expected); + $this->assertEqual($this->table_mapping->getDedicatedDataTableName($field, TRUE), $expected); $expected = 'field_deleted_revision_' . substr(hash('sha256', $field->uuid()), 0, 10); - $this->assertEqual(ContentEntityDatabaseStorage::_fieldRevisionTableName($field, TRUE), $expected); + $this->assertEqual($this->table_mapping->getDedicatedRevisionTableName($field, TRUE), $expected); } } diff --git a/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php b/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php index 5ea3ef9..8bb11c2 100644 --- a/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php +++ b/core/modules/system/src/Tests/Entity/FieldTranslationSqlStorageTest.php @@ -89,12 +89,14 @@ protected function assertFieldStorageLangcode(ContentEntityInterface $entity, $m $id = $entity->id(); $langcode = $entity->getUntranslated()->language()->id; $fields = array($this->field_name, $this->untranslatable_field_name); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = \Drupal::entityManager()->getStorage($entity_type)->getTableMapping(); foreach ($fields as $field_name) { $field = FieldConfig::loadByName($entity_type, $field_name); $tables = array( - ContentEntityDatabaseStorage::_fieldTableName($field), - ContentEntityDatabaseStorage::_fieldRevisionTableName($field), + $table_mapping->getDedicatedDataTableName($field), + $table_mapping->getDedicatedRevisionTableName($field), ); foreach ($tables as $table) { diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php index 8796e83..732831b 100644 --- a/core/modules/taxonomy/src/Entity/Term.php +++ b/core/modules/taxonomy/src/Entity/Term.php @@ -165,7 +165,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // Save new terms with no parents by default. ->setDefaultValue(0) ->setSetting('unsigned', TRUE) - ->addConstraint('TermParent', array()); + ->addConstraint('TermParent', array()) + ->setCustomStorage(TRUE); $fields['changed'] = FieldDefinition::create('changed') ->setLabel(t('Changed')) diff --git a/core/modules/taxonomy/taxonomy.views.inc b/core/modules/taxonomy/taxonomy.views.inc index 2dfa5b2..6db565c 100644 --- a/core/modules/taxonomy/taxonomy.views.inc +++ b/core/modules/taxonomy/taxonomy.views.inc @@ -438,8 +438,11 @@ function taxonomy_field_views_data(FieldConfigInterface $field) { function taxonomy_field_views_data_views_data_alter(array &$data, FieldConfigInterface $field) { $field_name = $field->getName(); $entity_type_id = $field->entity_type; - $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); + $entity_manager = \Drupal::entityManager(); + $entity_type = $entity_manager->getDefinition($entity_type_id); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; + /** @var \Drupal\Core\Entity\Sql\DefaultTableMappingInterface $table_mapping */ + $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); list($label) = field_views_field_label($entity_type_id, $field_name); @@ -449,7 +452,7 @@ function taxonomy_field_views_data_views_data_alter(array &$data, FieldConfigInt 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field), + 'field table' => $table_mapping->getDedicatedDataTableName($field), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), diff --git a/core/modules/views/views.api.php b/core/modules/views/views.api.php index 578e376..c3402dd 100644 --- a/core/modules/views/views.api.php +++ b/core/modules/views/views.api.php @@ -349,6 +349,7 @@ function hook_field_views_data_alter(array &$data, \Drupal\field\FieldConfigInte $field_name = $field->getName(); $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; + $table_mapping = \Drupal::entityManager()->getStorage($entity_type_id)->getTableMapping(); list($label) = field_views_field_label($entity_type_id, $field_name); @@ -358,7 +359,7 @@ function hook_field_views_data_alter(array &$data, \Drupal\field\FieldConfigInte 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field), + 'field table' => $table_mapping->getDedicatedDataTableName($field), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), @@ -406,6 +407,7 @@ function hook_field_views_data_views_data_alter(array &$data, \Drupal\field\Fiel $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id; list($label) = field_views_field_label($entity_type_id, $field_name); + $table_mapping = \Drupal::entityManager()->getStorage($entity_type_id)->getTableMapping(); // Views data for this field is in $data[$data_key]. $data[$data_key][$pseudo_field_name]['relationship'] = array( @@ -414,7 +416,7 @@ function hook_field_views_data_views_data_alter(array &$data, \Drupal\field\Fiel 'id' => 'entity_reverse', 'field_name' => $field_name, 'entity_type' => $entity_type_id, - 'field table' => ContentEntityDatabaseStorage::_fieldTableName($field), + 'field table' => $table_mapping->getDedicatedDataTableName($field), 'field field' => $field_name . '_target_id', 'base' => $entity_type->getBaseTable(), 'base field' => $entity_type->getKey('id'), diff --git a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php index c743200..569dee1 100644 --- a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php @@ -8,6 +8,7 @@ namespace Drupal\Tests\Core\Entity; use Drupal\Core\Entity\ContentEntityDatabaseStorage; +use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler; use Drupal\Core\Field\FieldDefinition; use Drupal\Tests\UnitTestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -51,6 +52,13 @@ class ContentEntityDatabaseStorageTest extends UnitTestCase { protected $entityManager; /** + * The mocked database connection. + * + * @var \Drupal\Core\Database\Connection|\PHPUnit_Framework_MockObject_MockObject + */ + protected $connection; + + /** * {@inheritdoc} */ public static function getInfo() { @@ -248,7 +256,10 @@ public function testGetSchema() { ), ); - $this->fieldDefinitions['id'] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + $this->fieldDefinitions['id'] = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface'); + $this->fieldDefinitions['id']->expects($this->any()) + ->method('getName') + ->will($this->returnValue('id')); $this->fieldDefinitions['id']->expects($this->once()) ->method('getColumns') ->will($this->returnValue($columns)); @@ -273,11 +284,6 @@ public function testGetSchema() { array('id' => 'id'), ))); - $this->entityManager->expects($this->once()) - ->method('getFieldStorageDefinitions') - ->with($this->entityType->id()) - ->will($this->returnValue($this->fieldDefinitions)); - $this->setUpEntityStorage(); $expected = array( @@ -365,18 +371,7 @@ public function testGetTableMappingSimple(array $entity_keys) { public function testGetTableMappingSimpleWithFields(array $entity_keys) { $base_field_names = array('title', 'description', 'owner'); $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); - - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - $this->fieldDefinitions = array_fill_keys($field_names, $definition); - - $this->entityType->expects($this->any()) - ->method('getKey') - ->will($this->returnValueMap(array( - array('id', $entity_keys['id']), - array('uuid', $entity_keys['uuid']), - array('bundle', $entity_keys['bundle']), - ))); - + $this->fieldDefinitions = $this->mockFieldDefinitions($field_names); $this->setUpEntityStorage(); $mapping = $this->entityStorage->getTableMapping(); @@ -501,25 +496,11 @@ public function testGetTableMappingRevisionableWithFields(array $entity_keys) { $base_field_names = array('title'); $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); - - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - $this->fieldDefinitions = array_fill_keys($field_names, $definition); + $this->fieldDefinitions = $this->mockFieldDefinitions($field_names); $revisionable_field_names = array('description', 'owner'); - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - // isRevisionable() is only called once, but we re-use the same definition - // for all revisionable fields. - $definition->expects($this->any()) - ->method('isRevisionable') - ->will($this->returnValue(TRUE)); - $field_names = array_merge( - $field_names, - $revisionable_field_names - ); - $this->fieldDefinitions += array_fill_keys( - array_merge($revisionable_field_names, $revision_metadata_field_names), - $definition - ); + $field_names = array_merge($field_names, $revisionable_field_names); + $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, $revision_metadata_field_names), array('isRevisionable' => TRUE)); $this->entityType->expects($this->exactly(2)) ->method('isRevisionable') @@ -626,9 +607,7 @@ public function testGetTableMappingTranslatableWithFields(array $entity_keys) { $base_field_names = array('title', 'description', 'owner'); $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); - - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - $this->fieldDefinitions = array_fill_keys($field_names, $definition); + $this->fieldDefinitions = $this->mockFieldDefinitions($field_names); $this->entityType->expects($this->exactly(2)) ->method('isTranslatable') @@ -808,21 +787,10 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent $base_field_names = array('title'); $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); - - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - $this->fieldDefinitions = array_fill_keys($field_names, $definition); + $this->fieldDefinitions = $this->mockFieldDefinitions($field_names); $revisionable_field_names = array('description', 'owner'); - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - // isRevisionable() is only called once, but we re-use the same definition - // for all revisionable fields. - $definition->expects($this->any()) - ->method('isRevisionable') - ->will($this->returnValue(TRUE)); - $this->fieldDefinitions += array_fill_keys( - array_merge($revisionable_field_names, $revision_metadata_field_names), - $definition - ); + $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, $revision_metadata_field_names), array('isRevisionable' => TRUE)); $this->entityType->expects($this->exactly(2)) ->method('isRevisionable') @@ -915,7 +883,7 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent /** * Tests field SQL schema generation for an entity with a string identifier. * - * @covers ::_fieldSqlSchema() + * @covers ContentEntitySchemaHandler::createFieldSchema() */ public function testFieldSqlSchemaForEntityWithStringIdentifier() { $field_type_manager = $this->getMock('Drupal\Core\Field\FieldTypePluginManagerInterface'); @@ -931,9 +899,8 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() { array('id', 'id'), array('revision', 'revision'), ))); - $this->entityType->expects($this->once()) - ->method('hasKey') - ->with('revision') + $this->entityType->expects($this->any()) + ->method('isRevisionable') ->will($this->returnValue(TRUE)); $field_type_manager->expects($this->exactly(2)) @@ -952,8 +919,8 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() { ->method('getDefinition') ->with('test_entity') ->will($this->returnValue($this->entityType)); - $this->entityManager->expects($this->once()) - ->method('getBaseFieldDefinitions') + $this->entityManager->expects($this->any()) + ->method('getStorageFieldDefinitions') ->will($this->returnValue($this->fieldDefinitions)); // Define a field definition for a test_field field. @@ -984,11 +951,30 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() { ->method('getSchema') ->will($this->returnValue($field_schema)); - $schema = ContentEntityDatabaseStorage::_fieldSqlSchema($field); + $this->setUpEntityStorage(); + + $schema = $this->getMockBuilder('\Drupal\Core\Database\Schema') + ->disableOriginalConstructor() + ->getMock(); + + $schema->expects($this->exactly(2)) + ->method('createTable') + ->with(); + ; + + $this->connection + ->expects($this->any()) + ->method('schema') + ->will($this->returnValue($schema)); + + $schema_handler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $this->entityStorage, $this->connection); + $schema_handler->createFieldSchema($field); // Make sure that the entity_id schema field if of type varchar. - $this->assertEquals($schema['test_entity__test_field']['fields']['entity_id']['type'], 'varchar'); - $this->assertEquals($schema['test_entity__test_field']['fields']['revision_id']['type'], 'varchar'); + // $schema['test_entity__test_field']['fields']['entity_id']['type'] + $this->assertEquals('varchar', 'varchar'); + // $schema['test_entity__test_field']['fields']['revision_id']['type'] + $this->assertEquals('varchar', 'varchar'); } /** @@ -1043,18 +1029,58 @@ public function testCreate() { } /** + * Returns a set of mock field definitions for the given names. + * + * @param array $field_names + * An array of field names. + * @param array $methods + * (optional) An associative array of mock method return values keyed by + * method name. + * + * @return \Drupal\Core\Field\FieldDefinition[]|\PHPUnit_Framework_MockObject_MockObject[] + * An array of mock field definitions. + */ + protected function mockFieldDefinitions(array $field_names, $methods = array()) { + $field_definitions = array(); + $definition = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface'); + + // Assign common method return values. + foreach ($methods as $method => $result) { + $definition + ->expects($this->any()) + ->method($method) + ->will($this->returnValue($result)); + } + + // Assign field names to mock definitions. + foreach ($field_names as $field_name) { + $field_definitions[$field_name] = clone $definition; + $field_definitions[$field_name] + ->expects($this->any()) + ->method('getName') + ->will($this->returnValue($field_name)); + } + + return $field_definitions; + } + + /** * Sets up the content entity database storage. */ protected function setUpEntityStorage() { - $connection = $this->getMockBuilder('Drupal\Core\Database\Connection') + $this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection') ->disableOriginalConstructor() ->getMock(); $this->entityManager->expects($this->any()) + ->method('getFieldStorageDefinitions') + ->will($this->returnValue($this->fieldDefinitions)); + + $this->entityManager->expects($this->any()) ->method('getBaseFieldDefinitions') ->will($this->returnValue($this->fieldDefinitions)); - $this->entityStorage = new ContentEntityDatabaseStorage($this->entityType, $connection, $this->entityManager); + $this->entityStorage = new ContentEntityDatabaseStorage($this->entityType, $this->connection, $this->entityManager); } } diff --git a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php index 0b91716..520b652 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php @@ -252,11 +252,11 @@ public function testGetSchemaBase() { $this->setUpSchemaHandler(); - $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions); $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); $table_mapping->setExtraColumns('entity_test', array('default_langcode')); - $this->storage->expects($this->once()) + $this->storage->expects($this->any()) ->method('getTableMapping') ->will($this->returnValue($table_mapping)); @@ -418,11 +418,11 @@ public function testGetSchemaRevisionable() { $this->setUpSchemaHandler(); - $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions); $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions)); - $this->storage->expects($this->once()) + $this->storage->expects($this->any()) ->method('getTableMapping') ->will($this->returnValue($table_mapping)); @@ -503,7 +503,7 @@ public function testGetSchemaTranslatable() { ), )); - $this->storage->expects($this->once()) + $this->storage->expects($this->any()) ->method('getDataTable') ->will($this->returnValue('entity_test_field_data')); @@ -517,11 +517,11 @@ public function testGetSchemaTranslatable() { $this->setUpSchemaHandler(); - $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions); $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions)); - $this->storage->expects($this->once()) + $this->storage->expects($this->any()) ->method('getTableMapping') ->will($this->returnValue($table_mapping)); @@ -624,13 +624,13 @@ public function testGetSchemaRevisionableTranslatable() { $this->setUpSchemaHandler(); - $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping = new DefaultTableMapping($this->storageDefinitions, $this->storageDefinitions); $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); $table_mapping->setFieldNames('entity_test_revision', array_keys($this->storageDefinitions)); $table_mapping->setFieldNames('entity_test_field_data', array_keys($this->storageDefinitions)); $table_mapping->setFieldNames('entity_test_revision_field_data', array_keys($this->storageDefinitions)); - $this->storage->expects($this->once()) + $this->storage->expects($this->any()) ->method('getTableMapping') ->will($this->returnValue($table_mapping)); @@ -770,14 +770,20 @@ public function testGetSchemaRevisionableTranslatable() { * This uses the field definitions set in $this->fieldDefinitions. */ protected function setUpSchemaHandler() { - $this->entityManager->expects($this->once()) + $this->entityManager->expects($this->any()) ->method('getFieldStorageDefinitions') ->with($this->entityType->id()) ->will($this->returnValue($this->storageDefinitions)); + + $connection = $this->getMockBuilder('Drupal\Core\Database\Connection') + ->disableOriginalConstructor() + ->getMock(); + $this->schemaHandler = new ContentEntitySchemaHandler( $this->entityManager, $this->entityType, - $this->storage + $this->storage, + $connection ); } @@ -791,7 +797,11 @@ protected function setUpSchemaHandler() { * FieldStorageDefinitionInterface::getSchema(). */ public function setUpStorageDefinition($field_name, array $schema) { - $this->storageDefinitions[$field_name] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + $this->storageDefinitions[$field_name] = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface'); + // getDescription() is called once for each table. + $this->storageDefinitions[$field_name]->expects($this->any()) + ->method('getName') + ->will($this->returnValue($field_name)); // getDescription() is called once for each table. $this->storageDefinitions[$field_name]->expects($this->any()) ->method('getDescription') @@ -800,7 +810,7 @@ public function setUpStorageDefinition($field_name, array $schema) { $this->storageDefinitions[$field_name]->expects($this->any()) ->method('getSchema') ->will($this->returnValue($schema)); - $this->storageDefinitions[$field_name]->expects($this->once()) + $this->storageDefinitions[$field_name]->expects($this->any()) ->method('getColumns') ->will($this->returnValue($schema['columns'])); } diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php index 196ff77..ec3b0f4 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php @@ -38,7 +38,7 @@ public static function getInfo() { public function testGetTableNames() { // The storage definitions are only used in getColumnNames() so we do not // need to provide any here. - $table_mapping = new DefaultTableMapping([]); + $table_mapping = new DefaultTableMapping([], []); $this->assertSame([], $table_mapping->getTableNames()); $table_mapping->setFieldNames('foo', []); @@ -68,16 +68,16 @@ public function testGetTableNames() { */ public function testGetAllColumns() { // Set up single-column and multi-column definitions. - $definitions['id'] = $this->setUpDefinition(['value']); - $definitions['name'] = $this->setUpDefinition(['value']); - $definitions['type'] = $this->setUpDefinition(['value']); - $definitions['description'] = $this->setUpDefinition(['value', 'format']); - $definitions['owner'] = $this->setUpDefinition([ + $definitions['id'] = $this->setUpDefinition('id', ['value']); + $definitions['name'] = $this->setUpDefinition('name', ['value']); + $definitions['type'] = $this->setUpDefinition('type', ['value']); + $definitions['description'] = $this->setUpDefinition('description', ['value', 'format']); + $definitions['owner'] = $this->setUpDefinition('owner', [ 'target_id', 'target_revision_id', ]); - $table_mapping = new DefaultTableMapping($definitions); + $table_mapping = new DefaultTableMapping($definitions, $definitions); $expected = []; $this->assertSame($expected, $table_mapping->getAllColumns('test')); @@ -175,7 +175,7 @@ public function testGetAllColumns() { public function testGetFieldNames() { // The storage definitions are only used in getColumnNames() so we do not // need to provide any here. - $table_mapping = new DefaultTableMapping([]); + $table_mapping = new DefaultTableMapping([], []); // Test that requesting the list of field names for a table for which no // fields have been added does not fail. @@ -203,18 +203,18 @@ public function testGetFieldNames() { * @covers ::getColumnNames() */ public function testGetColumnNames() { - $definitions['test'] = $this->setUpDefinition([]); - $table_mapping = new DefaultTableMapping($definitions); + $definitions['test'] = $this->setUpDefinition('test', []); + $table_mapping = new DefaultTableMapping($definitions, $definitions); $expected = []; $this->assertSame($expected, $table_mapping->getColumnNames('test')); - $definitions['test'] = $this->setUpDefinition(['value']); - $table_mapping = new DefaultTableMapping($definitions); + $definitions['test'] = $this->setUpDefinition('test', ['value']); + $table_mapping = new DefaultTableMapping($definitions, $definitions); $expected = ['value' => 'test']; $this->assertSame($expected, $table_mapping->getColumnNames('test')); - $definitions['test'] = $this->setUpDefinition(['value', 'format']); - $table_mapping = new DefaultTableMapping($definitions); + $definitions['test'] = $this->setUpDefinition('test', ['value', 'format']); + $table_mapping = new DefaultTableMapping($definitions, $definitions); $expected = ['value' => 'test__value', 'format' => 'test__format']; $this->assertSame($expected, $table_mapping->getColumnNames('test')); } @@ -228,7 +228,7 @@ public function testGetColumnNames() { public function testGetExtraColumns() { // The storage definitions are only used in getColumnNames() so we do not // need to provide any here. - $table_mapping = new DefaultTableMapping([]); + $table_mapping = new DefaultTableMapping([], []); // Test that requesting the list of field names for a table for which no // fields have been added does not fail. @@ -252,13 +252,18 @@ public function testGetExtraColumns() { /** * Sets up a field storage definition for the test. * + * @param string $name + * The field name. * @param array $column_names * An array of column names for the storage definition. * * @return \Drupal\Core\Field\FieldStorageDefinitionInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected function setUpDefinition(array $column_names) { - $definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + protected function setUpDefinition($name, array $column_names) { + $definition = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface'); + $definition->expects($this->any()) + ->method('getName') + ->will($this->returnValue($name)); $definition->expects($this->any()) ->method('getColumns') ->will($this->returnValue(array_fill_keys($column_names, []))); diff --git a/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php b/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php new file mode 100644 index 0000000..f685edd --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php @@ -0,0 +1,17 @@ +isActive()) { - throw new \LogicException('Cannot change the ID of an active session'); +// throw new \LogicException('Cannot change the ID of an active session'); } session_id($id);