/** * {@inheritdoc} */ public function getTableMapping(array $storage_definitions = NULL) { $table_mapping = $this->tableMapping; // If we are using our internal storage definitions, which is our main use // case, we can statically cache the computed table mapping. If a new set // of field storage definitions is passed, for instance when comparing old // and new storage schema, we compute the table mapping without caching. // @todo Clean-up this in https://www.drupal.org/node/2274017 so we can // easily instantiate a new table mapping whenever needed. if (!isset($this->tableMapping) || $storage_definitions) { $definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId); $table_mapping = new DefaultTableMapping($this->entityType, $definitions); $definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use($table_mapping) { return $table_mapping->allowsSharedTableStorage($definition); }); $key_fields = array_values(array_filter(array($this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey))); $all_fields = array_keys($definitions); $revisionable_fields = array_keys(array_filter($definitions, function (FieldStorageDefinitionInterface $definition) { return $definition->isRevisionable(); })); // Make sure the key fields come first in the list of fields. $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields)); // Nodes have all three of these fields, while custom blocks only have // log. // @todo Provide automatic definitions for revision metadata fields in // https://www.drupal.org/node/2248983. $revision_metadata_fields = array_intersect(array('revision_timestamp', 'revision_uid', 'revision_log'), $all_fields); $revisionable = $this->entityType->isRevisionable(); $translatable = $this->entityType->isTranslatable(); if (!$revisionable && !$translatable) { // The base layout stores all the base field values in the base table. $table_mapping->setFieldNames($this->baseTable, $all_fields); } elseif ($revisionable && !$translatable) { // The revisionable layout stores all the base field values in the base // table, except for revision metadata fields. Revisionable fields // denormalized in the base table but also stored in the revision table // together with the entity ID and the revision ID as identifiers. $table_mapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields)); $revision_key_fields = array($this->idKey, $this->revisionKey); $table_mapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields)); } elseif (!$revisionable && $translatable) { // Multilingual layouts store key field values in the base table. The // other base field values are stored in the data table, no matter // whether they are translatable or not. The data table holds also a // denormalized copy of the bundle field value to allow for more // performant queries. This means that only the UUID is not stored on // the data table. $table_mapping->setFieldNames($this->baseTable, $key_fields)->setFieldNames($this->dataTable, array_values(array_diff($all_fields, array($this->uuidKey)))); } elseif ($revisionable && $translatable) { // The revisionable multilingual layout stores key field values in the // base table, except for language, which is stored in the revision // table along with revision metadata. The revision data table holds // data field values for all the revisionable fields and the data table // holds the data field values for all non-revisionable fields. The data // field values of revisionable fields are denormalized in the data // table, as well. $table_mapping->setFieldNames($this->baseTable, array_values($key_fields)); // Like in the multilingual, non-revisionable case the UUID is not // in the data table. Additionally, do not store revision metadata // fields in the data table. $data_fields = array_values(array_diff($all_fields, array($this->uuidKey), $revision_metadata_fields)); $table_mapping->setFieldNames($this->dataTable, $data_fields); $revision_base_fields = array_merge(array($this->idKey, $this->revisionKey, $this->langcodeKey), $revision_metadata_fields); $table_mapping->setFieldNames($this->revisionTable, $revision_base_fields); $revision_data_key_fields = array($this->idKey, $this->revisionKey, $this->langcodeKey); $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, array($this->langcodeKey)); $table_mapping->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields)); } // Add dedicated tables. $definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use($table_mapping) { return $table_mapping->requiresDedicatedTableStorage($definition); }); $extra_columns = array('bundle', 'deleted', 'entity_id', 'revision_id', 'langcode', 'delta'); foreach ($definitions as $field_name => $definition) { foreach (array($table_mapping->getDedicatedDataTableName($definition), $table_mapping->getDedicatedRevisionTableName($definition)) as $table_name) { $table_mapping->setFieldNames($table_name, array($field_name)); $table_mapping->setExtraColumns($table_name, $extra_columns); } } // Cache the computed table mapping only if we are using our internal // storage definitions. if (!$storage_definitions) { $this->tableMapping = $table_mapping; } } return $table_mapping; }
/** * Tests DefaultTableMapping::getExtraColumns(). * * @covers ::getExtraColumns() * @covers ::setExtraColumns() */ public function testGetExtraColumns() { // The storage definitions are only used in getColumnNames() so we do not // need to provide any here. $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. $this->assertSame([], $table_mapping->getExtraColumns('foo')); $return = $table_mapping->setExtraColumns('foo', ['id', 'name', 'type']); $this->assertSame($table_mapping, $return); $expected = ['id', 'name', 'type']; $this->assertSame($expected, $table_mapping->getExtraColumns('foo')); $this->assertSame([], $table_mapping->getExtraColumns('bar')); $return = $table_mapping->setExtraColumns('bar', ['description', 'owner']); $this->assertSame($table_mapping, $return); $expected = ['description', 'owner']; $this->assertSame($expected, $table_mapping->getExtraColumns('bar')); // Test that the previously added field names are unaffected. $expected = ['id', 'name', 'type']; $this->assertSame($expected, $table_mapping->getExtraColumns('foo')); }
/** * ::onEntityTypeUpdate */ public function testonEntityTypeUpdateWithNewIndex() { $this->entityType = $original_entity_type = new ContentEntityType(array('id' => 'entity_test', 'entity_keys' => array('id' => 'id'))); // Add a field with a really long index. $this->setUpStorageDefinition('long_index_name', array('columns' => array('long_index_name' => array('type' => 'int')), 'indexes' => array('long_index_name_really_long_long_name' => array(array('long_index_name', 10))))); $expected = array('entity_test' => array('description' => 'The base table for entity_test entities.', 'fields' => array('id' => array('type' => 'serial', 'not null' => TRUE), 'long_index_name' => array('type' => 'int', 'not null' => FALSE)), 'indexes' => array('entity_test__b588603cb9' => array(array('long_index_name', 10))))); $this->setUpStorageSchema($expected); $table_mapping = new DefaultTableMapping($this->entityType, $this->storageDefinitions); $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); $table_mapping->setExtraColumns('entity_test', array('default_langcode')); $this->storage->expects($this->any())->method('getTableMapping')->will($this->returnValue($table_mapping)); $this->storageSchema->expects($this->any())->method('loadEntitySchemaData')->willReturn(['entity_test' => ['indexes' => ['entity_test__b588603cb9' => ['longer_index_name'], 'entity_test__removed_field' => ['removed_field']]]]); // The original indexes should be dropped before the new one is added. $this->dbSchemaHandler->expects($this->at(0))->method('dropIndex')->with('entity_test', 'entity_test__b588603cb9'); $this->dbSchemaHandler->expects($this->at(1))->method('dropIndex')->with('entity_test', 'entity_test__removed_field'); $this->dbSchemaHandler->expects($this->atLeastOnce())->method('fieldExists')->willReturn(TRUE); $this->dbSchemaHandler->expects($this->atLeastOnce())->method('addIndex')->with('entity_test', 'entity_test__b588603cb9', [['long_index_name', 10]], $this->callback(function ($actual_value) use($expected) { $this->assertEquals($expected['entity_test']['indexes'], $actual_value['indexes']); $this->assertEquals($expected['entity_test']['fields'], $actual_value['fields']); // If the parameters don't match, the assertions above will throw an // exception. return TRUE; })); $this->assertNull($this->storageSchema->onEntityTypeUpdate($this->entityType, $original_entity_type)); }
/** * Tests the schema for a field dedicated table for an entity with a string identifier. * * @covers ::getDedicatedTableSchema() * @covers ::createDedicatedTableSchema() */ public function testDedicatedTableSchemaForEntityWithStringIdentifier() { $entity_type_id = 'entity_test'; $this->entityType = new ContentEntityType(array('id' => 'entity_test', 'entity_keys' => array('id' => 'id'))); // Setup a field having a dedicated schema. $field_name = $this->getRandomGenerator()->name(); $this->setUpStorageDefinition($field_name, array('columns' => array('shape' => array('type' => 'varchar', 'length' => 32, 'not null' => FALSE), 'color' => array('type' => 'varchar', 'length' => 32, 'not null' => FALSE)), 'foreign keys' => array('color' => array('table' => 'color', 'columns' => array('color' => 'id'))), 'unique keys' => array(), 'indexes' => array())); $field_storage = $this->storageDefinitions[$field_name]; $field_storage->expects($this->any())->method('getType')->will($this->returnValue('shape')); $field_storage->expects($this->any())->method('getTargetEntityTypeId')->will($this->returnValue($entity_type_id)); $field_storage->expects($this->any())->method('isMultiple')->will($this->returnValue(TRUE)); $this->storageDefinitions['id']->expects($this->any())->method('getType')->will($this->returnValue('string')); $expected = array($entity_type_id . '__' . $field_name => array('description' => "Data storage for {$entity_type_id} field {$field_name}.", 'fields' => array('bundle' => array('type' => 'varchar', 'length' => 128, 'not null' => true, 'default' => '', 'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance'), 'deleted' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 0, 'description' => 'A boolean indicating whether this data item has been deleted'), 'entity_id' => array('type' => 'varchar', 'length' => 128, 'not null' => true, 'description' => 'The entity id this data is attached to'), 'revision_id' => array('type' => 'varchar', 'length' => 128, 'not null' => true, 'description' => 'The entity revision id this data is attached to, which for an unversioned entity type is the same as the entity id'), 'langcode' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'default' => '', 'description' => 'The language code for this data item.'), 'delta' => array('type' => 'int', 'unsigned' => true, 'not null' => true, 'description' => 'The sequence number for this data item, used for multi-value fields'), $field_name . '_shape' => array('type' => 'varchar', 'length' => 32, 'not null' => false), $field_name . '_color' => array('type' => 'varchar', 'length' => 32, 'not null' => false)), 'primary key' => array('entity_id', 'deleted', 'delta', 'langcode'), 'indexes' => array('bundle' => array('bundle'), 'deleted' => array('deleted'), 'entity_id' => array('entity_id'), 'revision_id' => array('revision_id'), 'langcode' => array('langcode')), 'foreign keys' => array($field_name . '_color' => array('table' => 'color', 'columns' => array($field_name . '_color' => 'id'))))); $this->setUpStorageSchema($expected); $table_mapping = new DefaultTableMapping($this->storageDefinitions); $table_mapping->setFieldNames($entity_type_id, array_keys($this->storageDefinitions)); $table_mapping->setExtraColumns($entity_type_id, array('default_langcode')); $this->storage->expects($this->any())->method('getTableMapping')->will($this->returnValue($table_mapping)); $this->storageSchema->onFieldStorageDefinitionCreate($field_storage); }
/** * @covers ::requiresEntityStorageSchemaChanges * * @dataProvider providerTestRequiresEntityStorageSchemaChanges */ public function testRequiresEntityStorageSchemaChanges(ContentEntityTypeInterface $updated, ContentEntityTypeInterface $original, $requires_change, $change_schema, $change_shared_table) { $this->entityType = new ContentEntityType(array('id' => 'entity_test', 'entity_keys' => array('id' => 'id'))); $this->setUpStorageSchema(); $table_mapping = new DefaultTableMapping($this->entityType, $this->storageDefinitions); $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); $table_mapping->setExtraColumns('entity_test', array('default_langcode')); $this->storage->expects($this->any())->method('getTableMapping')->will($this->returnValue($table_mapping)); // Setup storage schema. if ($change_schema) { $this->storageSchema->expects($this->once())->method('loadEntitySchemaData')->willReturn(array()); } else { $expected = ['entity_test' => ['primary key' => ['id']]]; $this->storageSchema->expects($this->any())->method('loadEntitySchemaData')->willReturn($expected); } if ($change_shared_table) { $this->storageSchema->expects($this->once())->method('hasSharedTableNameChanges')->willReturn(TRUE); } $this->assertEquals($requires_change, $this->storageSchema->requiresEntityStorageSchemaChanges($updated, $original)); }
/** * Tests the schema for non-revisionable, non-translatable entities. * * @covers ::__construct() * @covers ::getSchema() * @covers ::getTables() * @covers ::initializeBaseTable() * @covers ::addTableDefaults() * @covers ::getEntityIndexName() * @covers ::addFieldSchema() * @covers ::getFieldIndexes() * @covers ::getFieldUniqueKeys() * @covers ::getFieldForeignKeys() * @covers ::getFieldSchemaData() * @covers ::addDefaultLangcodeSchema() * @covers ::processBaseTable() * @covers ::processIdentifierSchema() */ public function testGetSchemaBase() { $this->entityType = new ContentEntityType(array('id' => 'entity_test', 'entity_keys' => array('id' => 'id'))); // Add a field with a 'length' constraint. $this->setUpStorageDefinition('name', array('columns' => array('value' => array('type' => 'varchar', 'length' => 255)))); // Add a multi-column field. $this->setUpStorageDefinition('description', array('columns' => array('value' => array('type' => 'text', 'description' => 'The text value'), 'format' => array('type' => 'varchar', 'description' => 'The text description')))); // Add a field with a unique key. $this->setUpStorageDefinition('uuid', array('columns' => array('value' => array('type' => 'varchar', 'length' => 128)), 'unique keys' => array('value' => array('value')))); // Add a field with a unique key, specified as column name and length. $this->setUpStorageDefinition('hash', array('columns' => array('value' => array('type' => 'varchar', 'length' => 20)), 'unique keys' => array('value' => array(array('value', 10))))); // Add a field with a multi-column unique key. $this->setUpStorageDefinition('email', array('columns' => array('username' => array('type' => 'varchar'), 'hostname' => array('type' => 'varchar'), 'domain' => array('type' => 'varchar')), 'unique keys' => array('email' => array('username', 'hostname', array('domain', 3))))); // Add a field with an index. $this->setUpStorageDefinition('owner', array('columns' => array('target_id' => array('type' => 'int')), 'indexes' => array('target_id' => array('target_id')))); // Add a field with an index, specified as column name and length. $this->setUpStorageDefinition('translator', array('columns' => array('target_id' => array('type' => 'int')), 'indexes' => array('target_id' => array(array('target_id', 10))))); // Add a field with a multi-column index. $this->setUpStorageDefinition('location', array('columns' => array('country' => array('type' => 'varchar'), 'state' => array('type' => 'varchar'), 'city' => array('type' => 'varchar')), 'indexes' => array('country_state_city' => array('country', 'state', array('city', 10))))); // Add a field with a foreign key. $this->setUpStorageDefinition('editor', array('columns' => array('target_id' => array('type' => 'int')), 'foreign keys' => array('user_id' => array('table' => 'users', 'columns' => array('target_id' => 'uid'))))); // Add a multi-column field with a foreign key. $this->setUpStorageDefinition('editor_revision', array('columns' => array('target_id' => array('type' => 'int'), 'target_revision_id' => array('type' => 'int')), 'foreign keys' => array('user_id' => array('table' => 'users', 'columns' => array('target_id' => 'uid'))))); // Add a field with a really long index. $this->setUpStorageDefinition('long_index_name', array('columns' => array('long_index_name' => array('type' => 'int')), 'indexes' => array('long_index_name_really_long_long_name' => array(array('long_index_name', 10))))); $this->setUpSchemaHandler(); $table_mapping = new DefaultTableMapping($this->storageDefinitions); $table_mapping->setFieldNames('entity_test', array_keys($this->storageDefinitions)); $table_mapping->setExtraColumns('entity_test', array('default_langcode')); $this->storage->expects($this->once())->method('getTableMapping')->will($this->returnValue($table_mapping)); $expected = array('entity_test' => array('description' => 'The base table for entity_test entities.', 'fields' => array('id' => array('description' => 'The id field.', 'type' => 'serial', 'not null' => TRUE), 'name' => array('description' => 'The name field.', 'type' => 'varchar', 'length' => 255), 'description__value' => array('description' => 'The description field.', 'type' => 'text'), 'description__format' => array('description' => 'The description field.', 'type' => 'varchar'), 'uuid' => array('description' => 'The uuid field.', 'type' => 'varchar', 'length' => 128), 'hash' => array('description' => 'The hash field.', 'type' => 'varchar', 'length' => 20), 'email__username' => array('description' => 'The email field.', 'type' => 'varchar'), 'email__hostname' => array('description' => 'The email field.', 'type' => 'varchar'), 'email__domain' => array('description' => 'The email field.', 'type' => 'varchar'), 'owner' => array('description' => 'The owner field.', 'type' => 'int'), 'translator' => array('description' => 'The translator field.', 'type' => 'int'), 'location__country' => array('description' => 'The location field.', 'type' => 'varchar'), 'location__state' => array('description' => 'The location field.', 'type' => 'varchar'), 'location__city' => array('description' => 'The location field.', 'type' => 'varchar'), 'editor' => array('description' => 'The editor field.', 'type' => 'int'), 'editor_revision__target_id' => array('description' => 'The editor_revision field.', 'type' => 'int'), 'editor_revision__target_revision_id' => array('description' => 'The editor_revision field.', 'type' => 'int'), 'long_index_name' => array('description' => 'The long_index_name field.', 'type' => 'int'), 'default_langcode' => array('description' => 'Boolean indicating whether field values are in the default entity language.', 'type' => 'int', 'size' => 'tiny', 'not null' => TRUE, 'default' => 1)), 'primary key' => array('id'), 'unique keys' => array('entity_test_field__uuid__value' => array('uuid'), 'entity_test_field__hash__value' => array(array('hash', 10)), 'entity_test_field__email__email' => array('email__username', 'email__hostname', array('email__domain', 3))), 'indexes' => array('entity_test_field__owner__target_id' => array('owner'), 'entity_test_field__translator__target_id' => array(array('translator', 10)), 'entity_test_field__location__country_state_city' => array('location__country', 'location__state', array('location__city', 10)), 'entity_test__b588603cb9' => array(array('long_index_name', 10))), 'foreign keys' => array('entity_test_field__editor__user_id' => array('table' => 'users', 'columns' => array('editor' => 'uid')), 'entity_test_field__editor_revision__user_id' => array('table' => 'users', 'columns' => array('editor_revision__target_id' => 'uid'))))); $actual = $this->schemaHandler->getSchema(); $this->assertEquals($expected, $actual); }